Merge "Revert "Fixed UserVisibilityMediator profile scenario.""
diff --git a/core/api/current.txt b/core/api/current.txt
index af2dd3d..5f1ac76 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -4757,6 +4757,7 @@
method public int getLaunchDisplayId();
method public boolean getLockTaskMode();
method public int getPendingIntentBackgroundActivityStartMode();
+ method public int getPendingIntentCreatorBackgroundActivityStartMode();
method public int getSplashScreenStyle();
method @Deprecated public boolean isPendingIntentBackgroundActivityLaunchAllowed();
method public boolean isShareIdentityEnabled();
@@ -4777,6 +4778,7 @@
method public android.app.ActivityOptions setLockTaskEnabled(boolean);
method @Deprecated public void setPendingIntentBackgroundActivityLaunchAllowed(boolean);
method @NonNull public android.app.ActivityOptions setPendingIntentBackgroundActivityStartMode(int);
+ method @NonNull public android.app.ActivityOptions setPendingIntentCreatorBackgroundActivityStartMode(int);
method @NonNull public android.app.ActivityOptions setShareIdentityEnabled(boolean);
method @NonNull public android.app.ActivityOptions setSplashScreenStyle(int);
method public android.os.Bundle toBundle();
@@ -24633,7 +24635,8 @@
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.media.RouteListingPreference.Item> CREATOR;
field public static final int FLAG_ONGOING_SESSION = 1; // 0x1
- field public static final int FLAG_SUGGESTED_ROUTE = 2; // 0x2
+ field public static final int FLAG_ONGOING_SESSION_MANAGED = 2; // 0x2
+ field public static final int FLAG_SUGGESTED_ROUTE = 4; // 0x4
field public static final int SELECTION_BEHAVIOR_GO_TO_APP = 2; // 0x2
field public static final int SELECTION_BEHAVIOR_NONE = 0; // 0x0
field public static final int SELECTION_BEHAVIOR_TRANSFER = 1; // 0x1
@@ -26327,8 +26330,23 @@
package android.media.tv {
+ public final class AdBuffer implements android.os.Parcelable {
+ ctor public AdBuffer(int, @NonNull String, @NonNull android.os.SharedMemory, int, int, long, int);
+ method public int describeContents();
+ method public int getFlags();
+ method public int getId();
+ method public int getLength();
+ method @NonNull public String getMimeType();
+ method public int getOffset();
+ method public long getPresentationTimeUs();
+ method @NonNull public android.os.SharedMemory getSharedMemory();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.media.tv.AdBuffer> CREATOR;
+ }
+
public final class AdRequest implements android.os.Parcelable {
ctor public AdRequest(int, int, @Nullable android.os.ParcelFileDescriptor, long, long, long, @Nullable String, @NonNull android.os.Bundle);
+ ctor public AdRequest(int, int, @Nullable android.net.Uri, long, long, long, @NonNull android.os.Bundle);
method public int describeContents();
method public long getEchoIntervalMillis();
method @Nullable public android.os.ParcelFileDescriptor getFileDescriptor();
@@ -26338,6 +26356,7 @@
method public int getRequestType();
method public long getStartTimeMillis();
method public long getStopTimeMillis();
+ method @Nullable public android.net.Uri getUri();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.media.tv.AdRequest> CREATOR;
field public static final int REQUEST_TYPE_START = 1; // 0x1
@@ -26352,6 +26371,7 @@
method public int getResponseType();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.media.tv.AdResponse> CREATOR;
+ field public static final int RESPONSE_TYPE_BUFFERING = 5; // 0x5
field public static final int RESPONSE_TYPE_ERROR = 4; // 0x4
field public static final int RESPONSE_TYPE_FINISHED = 2; // 0x2
field public static final int RESPONSE_TYPE_PLAYING = 1; // 0x1
@@ -27105,6 +27125,7 @@
public abstract static class TvInputService.Session implements android.view.KeyEvent.Callback {
ctor public TvInputService.Session(android.content.Context);
method public void layoutSurface(int, int, int, int);
+ method public void notifyAdBufferConsumed(@NonNull android.media.tv.AdBuffer);
method public void notifyAdResponse(@NonNull android.media.tv.AdResponse);
method public void notifyAitInfoUpdated(@NonNull android.media.tv.AitInfo);
method public void notifyAudioPresentationChanged(@NonNull java.util.List<android.media.AudioPresentation>);
@@ -27120,6 +27141,7 @@
method public void notifyTuned(@NonNull android.net.Uri);
method public void notifyVideoAvailable();
method public void notifyVideoUnavailable(int);
+ method public void onAdBuffer(@NonNull android.media.tv.AdBuffer);
method public void onAppPrivateCommand(@NonNull String, android.os.Bundle);
method public android.view.View onCreateOverlayView();
method public boolean onGenericMotionEvent(android.view.MotionEvent);
@@ -27406,9 +27428,11 @@
ctor public TvInteractiveAppService.Session(@NonNull android.content.Context);
method public boolean isMediaViewEnabled();
method @CallSuper public void layoutSurface(int, int, int, int);
+ method @CallSuper public void notifyAdBuffer(@NonNull android.media.tv.AdBuffer);
method @CallSuper public final void notifyBiInteractiveAppCreated(@NonNull android.net.Uri, @Nullable String);
method @CallSuper public void notifySessionStateChanged(int, int);
method @CallSuper public final void notifyTeletextAppStateChanged(int);
+ method public void onAdBufferConsumed(@NonNull android.media.tv.AdBuffer);
method public void onAdResponse(@NonNull android.media.tv.AdResponse);
method public void onBroadcastInfoResponse(@NonNull android.media.tv.BroadcastInfoResponse);
method public void onContentAllowed();
@@ -40311,7 +40335,7 @@
ctor public GetCredentialRequest(@NonNull android.service.credentials.CallingAppInfo, @NonNull android.credentials.CredentialOption);
method public int describeContents();
method @NonNull public android.service.credentials.CallingAppInfo getCallingAppInfo();
- method @NonNull public android.credentials.CredentialOption getGetCredentialOption();
+ method @NonNull public android.credentials.CredentialOption getCredentialOption();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.service.credentials.GetCredentialRequest> CREATOR;
}
@@ -42470,7 +42494,7 @@
method @NonNull public String getDisplayName();
method @NonNull public android.os.Bundle getExtras();
method public int getState();
- method public void setStreamingState(int);
+ method public void requestStreamingState(int);
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.telecom.StreamingCall> CREATOR;
field public static final int STATE_DISCONNECTED = 3; // 0x3
@@ -45371,6 +45395,7 @@
field public static final String EXTRA_HIDE_PUBLIC_SETTINGS = "android.telephony.extra.HIDE_PUBLIC_SETTINGS";
field @Deprecated public static final String EXTRA_INCOMING_NUMBER = "incoming_number";
field public static final String EXTRA_IS_REFRESH = "android.telephony.extra.IS_REFRESH";
+ field public static final String EXTRA_LAST_KNOWN_NETWORK_COUNTRY = "android.telephony.extra.LAST_KNOWN_NETWORK_COUNTRY";
field public static final String EXTRA_LAUNCH_VOICEMAIL_SETTINGS_INTENT = "android.telephony.extra.LAUNCH_VOICEMAIL_SETTINGS_INTENT";
field public static final String EXTRA_NETWORK_COUNTRY = "android.telephony.extra.NETWORK_COUNTRY";
field public static final String EXTRA_NOTIFICATION_COUNT = "android.telephony.extra.NOTIFICATION_COUNT";
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index d0c000e..4a0b2eb 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -9783,6 +9783,133 @@
}
+package android.net.wifi.sharedconnectivity.app {
+
+ public final class DeviceInfo implements android.os.Parcelable {
+ method public int describeContents();
+ method @IntRange(from=0, to=100) @NonNull public int getBatteryPercentage();
+ method @IntRange(from=0, to=3) @NonNull public int getConnectionStrength();
+ method @NonNull public String getDeviceName();
+ method public int getDeviceType();
+ method @NonNull public String getModelName();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.net.wifi.sharedconnectivity.app.DeviceInfo> CREATOR;
+ field public static final int DEVICE_TYPE_LAPTOP = 3; // 0x3
+ field public static final int DEVICE_TYPE_PHONE = 1; // 0x1
+ field public static final int DEVICE_TYPE_TABLET = 2; // 0x2
+ field public static final int DEVICE_TYPE_UNKNOWN = 0; // 0x0
+ }
+
+ public static final class DeviceInfo.Builder {
+ ctor public DeviceInfo.Builder();
+ method @NonNull public android.net.wifi.sharedconnectivity.app.DeviceInfo build();
+ method @NonNull public android.net.wifi.sharedconnectivity.app.DeviceInfo.Builder setBatteryPercentage(@IntRange(from=0, to=100) int);
+ method @NonNull public android.net.wifi.sharedconnectivity.app.DeviceInfo.Builder setConnectionStrength(@IntRange(from=0, to=3) int);
+ method @NonNull public android.net.wifi.sharedconnectivity.app.DeviceInfo.Builder setDeviceName(@NonNull String);
+ method @NonNull public android.net.wifi.sharedconnectivity.app.DeviceInfo.Builder setDeviceType(int);
+ method @NonNull public android.net.wifi.sharedconnectivity.app.DeviceInfo.Builder setModelName(@NonNull String);
+ }
+
+ public final class KnownNetwork implements android.os.Parcelable {
+ method public int describeContents();
+ method @NonNull public android.net.wifi.sharedconnectivity.app.DeviceInfo getDeviceInfo();
+ method @NonNull public int getNetworkSource();
+ method @NonNull public int[] getSecurityTypes();
+ method @NonNull public String getSsid();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.net.wifi.sharedconnectivity.app.KnownNetwork> CREATOR;
+ field public static final int NETWORK_SOURCE_CLOUD_SELF = 1; // 0x1
+ field public static final int NETWORK_SOURCE_NEARBY_SELF = 0; // 0x0
+ }
+
+ public static final class KnownNetwork.Builder {
+ ctor public KnownNetwork.Builder();
+ method @NonNull public android.net.wifi.sharedconnectivity.app.KnownNetwork build();
+ method @NonNull public android.net.wifi.sharedconnectivity.app.KnownNetwork.Builder setDeviceInfo(@NonNull android.net.wifi.sharedconnectivity.app.DeviceInfo);
+ method @NonNull public android.net.wifi.sharedconnectivity.app.KnownNetwork.Builder setNetworkSource(int);
+ method @NonNull public android.net.wifi.sharedconnectivity.app.KnownNetwork.Builder setSecurityTypes(@NonNull int[]);
+ method @NonNull public android.net.wifi.sharedconnectivity.app.KnownNetwork.Builder setSsid(@NonNull String);
+ }
+
+ public interface SharedConnectivityClientCallback {
+ method public void onKnownNetworksUpdated(@NonNull java.util.List<android.net.wifi.sharedconnectivity.app.KnownNetwork>);
+ method public void onSharedConnectivitySettingsChanged(@NonNull android.net.wifi.sharedconnectivity.app.SharedConnectivitySettingsState);
+ method public void onTetherNetworksUpdated(@NonNull java.util.List<android.net.wifi.sharedconnectivity.app.TetherNetwork>);
+ }
+
+ public class SharedConnectivityManager {
+ ctor public SharedConnectivityManager(@NonNull android.content.Context);
+ method public boolean connectKnownNetwork(@NonNull android.net.wifi.sharedconnectivity.app.KnownNetwork);
+ method public boolean connectTetherNetwork(@NonNull android.net.wifi.sharedconnectivity.app.TetherNetwork);
+ method public boolean disconnectTetherNetwork();
+ method public boolean forgetKnownNetwork(@NonNull android.net.wifi.sharedconnectivity.app.KnownNetwork);
+ method public boolean registerCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.wifi.sharedconnectivity.app.SharedConnectivityClientCallback);
+ method public boolean unregisterCallback(@NonNull android.net.wifi.sharedconnectivity.app.SharedConnectivityClientCallback);
+ }
+
+ public final class SharedConnectivitySettingsState implements android.os.Parcelable {
+ method public int describeContents();
+ method @NonNull public android.os.Bundle getExtras();
+ method @NonNull public boolean isInstantTetherEnabled();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.net.wifi.sharedconnectivity.app.SharedConnectivitySettingsState> CREATOR;
+ }
+
+ public static final class SharedConnectivitySettingsState.Builder {
+ ctor public SharedConnectivitySettingsState.Builder();
+ method @NonNull public android.net.wifi.sharedconnectivity.app.SharedConnectivitySettingsState build();
+ method @NonNull public android.net.wifi.sharedconnectivity.app.SharedConnectivitySettingsState.Builder setExtras(@NonNull android.os.Bundle);
+ method @NonNull public android.net.wifi.sharedconnectivity.app.SharedConnectivitySettingsState.Builder setInstantTetherEnabled(boolean);
+ }
+
+ public final class TetherNetwork implements android.os.Parcelable {
+ method public int describeContents();
+ method @NonNull public long getDeviceId();
+ method @NonNull public android.net.wifi.sharedconnectivity.app.DeviceInfo getDeviceInfo();
+ method @Nullable public String getHotspotBssid();
+ method @Nullable public int[] getHotspotSecurityTypes();
+ method @Nullable public String getHotspotSsid();
+ method @NonNull public String getNetworkName();
+ method @NonNull public int getNetworkType();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.net.wifi.sharedconnectivity.app.TetherNetwork> CREATOR;
+ field public static final int NETWORK_TYPE_CELLULAR = 1; // 0x1
+ field public static final int NETWORK_TYPE_ETHERNET = 3; // 0x3
+ field public static final int NETWORK_TYPE_UNKNOWN = 0; // 0x0
+ field public static final int NETWORK_TYPE_WIFI = 2; // 0x2
+ }
+
+ public static final class TetherNetwork.Builder {
+ ctor public TetherNetwork.Builder();
+ method @NonNull public android.net.wifi.sharedconnectivity.app.TetherNetwork build();
+ method @NonNull public android.net.wifi.sharedconnectivity.app.TetherNetwork.Builder setDeviceId(long);
+ method @NonNull public android.net.wifi.sharedconnectivity.app.TetherNetwork.Builder setDeviceInfo(@NonNull android.net.wifi.sharedconnectivity.app.DeviceInfo);
+ method @NonNull public android.net.wifi.sharedconnectivity.app.TetherNetwork.Builder setHotspotBssid(@NonNull String);
+ method @NonNull public android.net.wifi.sharedconnectivity.app.TetherNetwork.Builder setHotspotSecurityTypes(@NonNull int[]);
+ method @NonNull public android.net.wifi.sharedconnectivity.app.TetherNetwork.Builder setHotspotSsid(@NonNull String);
+ method @NonNull public android.net.wifi.sharedconnectivity.app.TetherNetwork.Builder setNetworkName(@NonNull String);
+ method @NonNull public android.net.wifi.sharedconnectivity.app.TetherNetwork.Builder setNetworkType(int);
+ }
+
+}
+
+package android.net.wifi.sharedconnectivity.service {
+
+ public abstract class SharedConnectivityService extends android.app.Service {
+ ctor public SharedConnectivityService();
+ ctor public SharedConnectivityService(@NonNull android.os.Handler);
+ method @Nullable public android.os.IBinder onBind(@NonNull android.content.Intent);
+ method public abstract void onConnectKnownNetwork(@NonNull android.net.wifi.sharedconnectivity.app.KnownNetwork);
+ method public abstract void onConnectTetherNetwork(@NonNull android.net.wifi.sharedconnectivity.app.TetherNetwork);
+ method public abstract void onDisconnectTetherNetwork();
+ method public abstract void onForgetKnownNetwork(@NonNull android.net.wifi.sharedconnectivity.app.KnownNetwork);
+ method public final void setKnownNetworks(@NonNull java.util.List<android.net.wifi.sharedconnectivity.app.KnownNetwork>);
+ method public final void setSettingsState(@NonNull android.net.wifi.sharedconnectivity.app.SharedConnectivitySettingsState);
+ method public final void setTetherNetworks(@NonNull java.util.List<android.net.wifi.sharedconnectivity.app.TetherNetwork>);
+ }
+
+}
+
package android.nfc {
public final class NfcAdapter {
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 235bf8d..b3e3f35 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -144,8 +144,6 @@
method @RequiresPermission(android.Manifest.permission.CHANGE_CONFIGURATION) public boolean updateMccMncConfiguration(@NonNull String, @NonNull String);
method @RequiresPermission(android.Manifest.permission.DUMP) public void waitForBroadcastIdle();
field public static final long LOCK_DOWN_CLOSE_SYSTEM_DIALOGS = 174664365L; // 0xa692aadL
- field public static final int PROCESS_CAPABILITY_ALL = 15; // 0xf
- field public static final int PROCESS_CAPABILITY_ALL_EXPLICIT = 1; // 0x1
field public static final int PROCESS_CAPABILITY_ALL_IMPLICIT = 6; // 0x6
field public static final int PROCESS_CAPABILITY_NETWORK = 8; // 0x8
field public static final int PROCESS_STATE_FOREGROUND_SERVICE = 4; // 0x4
@@ -1865,6 +1863,14 @@
}
+package android.net.wifi.sharedconnectivity.app {
+
+ public class SharedConnectivityManager {
+ method public void setService(@Nullable android.os.IInterface);
+ }
+
+}
+
package android.os {
public final class BatteryStatsManager {
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index c4d6ad7..183357a 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -630,6 +630,8 @@
PROCESS_CAPABILITY_FOREGROUND_LOCATION,
PROCESS_CAPABILITY_FOREGROUND_CAMERA,
PROCESS_CAPABILITY_FOREGROUND_MICROPHONE,
+ PROCESS_CAPABILITY_NETWORK,
+ PROCESS_CAPABILITY_BFSL,
})
@Retention(RetentionPolicy.SOURCE)
public @interface ProcessCapability {}
@@ -654,20 +656,36 @@
@TestApi
public static final int PROCESS_CAPABILITY_NETWORK = 1 << 3;
- /** @hide all capabilities, the ORing of all flags in {@link ProcessCapability}*/
- @TestApi
+ /**
+ * Flag used to indicate whether an app is allowed to start a foreground service from the
+ * background, decided by the procstates. ("BFSL" == "background foreground service launch")
+ *
+ * - BFSL has a number of exemptions -- e.g. when an app is power-allowlisted, including
+ * temp-allowlist -- but this capability is *not* used to represent such exemptions.
+ * This is set only based on the procstate and the foreground service type.
+ * - Basically, procstates <= BFGS (i.e. BFGS, FGS, BTOP, TOP, ...) are BFSL-allowed,
+ * and that's how things worked on Android S/T.
+ * However, Android U added a "SHORT_SERVICE" FGS type, which gets the FGS procstate
+ * *but* can't start another FGS. So now we use this flag to decide whether FGS/BFGS
+ * procstates are BFSL-allowed. (higher procstates, such as BTOP, will still always be
+ * BFSL-allowed.)
+ * We propagate this flag across via service bindings and provider references.
+ *
+ * @hide
+ */
+ public static final int PROCESS_CAPABILITY_BFSL = 1 << 4;
+
+ /**
+ * @hide all capabilities, the ORing of all flags in {@link ProcessCapability}.
+ *
+ * Don't expose it as TestApi -- we may add new capabilities any time, which could
+ * break CTS tests if they relied on it.
+ */
public static final int PROCESS_CAPABILITY_ALL = PROCESS_CAPABILITY_FOREGROUND_LOCATION
| PROCESS_CAPABILITY_FOREGROUND_CAMERA
| PROCESS_CAPABILITY_FOREGROUND_MICROPHONE
- | PROCESS_CAPABILITY_NETWORK;
- /**
- * All explicit capabilities. These are capabilities that need to be specified from manifest
- * file.
- * @hide
- */
- @TestApi
- public static final int PROCESS_CAPABILITY_ALL_EXPLICIT =
- PROCESS_CAPABILITY_FOREGROUND_LOCATION;
+ | PROCESS_CAPABILITY_NETWORK
+ | PROCESS_CAPABILITY_BFSL;
/**
* All implicit capabilities. There are capabilities that process automatically have.
@@ -686,6 +704,7 @@
pw.print((caps & PROCESS_CAPABILITY_FOREGROUND_CAMERA) != 0 ? 'C' : '-');
pw.print((caps & PROCESS_CAPABILITY_FOREGROUND_MICROPHONE) != 0 ? 'M' : '-');
pw.print((caps & PROCESS_CAPABILITY_NETWORK) != 0 ? 'N' : '-');
+ pw.print((caps & PROCESS_CAPABILITY_BFSL) != 0 ? 'F' : '-');
}
/** @hide */
@@ -694,6 +713,7 @@
sb.append((caps & PROCESS_CAPABILITY_FOREGROUND_CAMERA) != 0 ? 'C' : '-');
sb.append((caps & PROCESS_CAPABILITY_FOREGROUND_MICROPHONE) != 0 ? 'M' : '-');
sb.append((caps & PROCESS_CAPABILITY_NETWORK) != 0 ? 'N' : '-');
+ sb.append((caps & PROCESS_CAPABILITY_BFSL) != 0 ? 'F' : '-');
}
/**
@@ -702,13 +722,10 @@
*/
public static void printCapabilitiesFull(PrintWriter pw, @ProcessCapability int caps) {
printCapabilitiesSummary(pw, caps);
- final int remain = caps & ~(PROCESS_CAPABILITY_FOREGROUND_LOCATION
- | PROCESS_CAPABILITY_FOREGROUND_CAMERA
- | PROCESS_CAPABILITY_FOREGROUND_MICROPHONE
- | PROCESS_CAPABILITY_NETWORK);
+ final int remain = caps & ~PROCESS_CAPABILITY_ALL;
if (remain != 0) {
- pw.print('+');
- pw.print(remain);
+ pw.print("+0x");
+ pw.print(Integer.toHexString(remain));
}
}
diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java
index b57fb20..b3337b6 100644
--- a/core/java/android/app/ActivityOptions.java
+++ b/core/java/android/app/ActivityOptions.java
@@ -397,8 +397,8 @@
/** See {@link #setDismissKeyguard()}. */
private static final String KEY_DISMISS_KEYGUARD = "android.activity.dismissKeyguard";
- private static final String KEY_IGNORE_PENDING_INTENT_CREATOR_FOREGROUND_STATE =
- "android.activity.ignorePendingIntentCreatorForegroundState";
+ private static final String KEY_PENDING_INTENT_CREATOR_BACKGROUND_ACTIVITY_START_MODE =
+ "android.activity.pendingIntentCreatorBackgroundActivityStartMode";
/**
* @see #setLaunchCookie
@@ -499,7 +499,9 @@
private boolean mTransientLaunch;
private PictureInPictureParams mLaunchIntoPipParams;
private boolean mDismissKeyguard;
- private boolean mIgnorePendingIntentCreatorForegroundState;
+ @BackgroundActivityStartMode
+ private int mPendingIntentCreatorBackgroundActivityStartMode =
+ MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED;
private boolean mDisableStartingWindow;
/**
@@ -1307,8 +1309,9 @@
mIsEligibleForLegacyPermissionPrompt =
opts.getBoolean(KEY_LEGACY_PERMISSION_PROMPT_ELIGIBLE);
mDismissKeyguard = opts.getBoolean(KEY_DISMISS_KEYGUARD);
- mIgnorePendingIntentCreatorForegroundState = opts.getBoolean(
- KEY_IGNORE_PENDING_INTENT_CREATOR_FOREGROUND_STATE);
+ mPendingIntentCreatorBackgroundActivityStartMode = opts.getInt(
+ KEY_PENDING_INTENT_CREATOR_BACKGROUND_ACTIVITY_START_MODE,
+ MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED);
mDisableStartingWindow = opts.getBoolean(KEY_DISABLE_STARTING_WINDOW);
}
@@ -2009,19 +2012,38 @@
* Sets background activity launch logic won't use pending intent creator foreground state.
*
* @hide
+ * @deprecated use {@link #setPendingIntentCreatorBackgroundActivityStartMode(int)} instead
*/
- public ActivityOptions setIgnorePendingIntentCreatorForegroundState(boolean state) {
- mIgnorePendingIntentCreatorForegroundState = state;
+ @Deprecated
+ public ActivityOptions setIgnorePendingIntentCreatorForegroundState(boolean ignore) {
+ mPendingIntentCreatorBackgroundActivityStartMode = ignore
+ ? MODE_BACKGROUND_ACTIVITY_START_DENIED : MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
return this;
}
/**
- * @return whether background activity launch logic should use pending intent creator
- * foreground state.
- * @hide
+ * Allow a {@link PendingIntent} to use the privilege of its creator to start background
+ * activities.
+ *
+ * @param mode the {@link android.app.ComponentOptions.BackgroundActivityStartMode} being set
+ * @throws IllegalArgumentException is the value is not a valid
+ * {@link android.app.ComponentOptions.BackgroundActivityStartMode}
*/
- public boolean getIgnorePendingIntentCreatorForegroundState() {
- return mIgnorePendingIntentCreatorForegroundState;
+ @NonNull
+ public ActivityOptions setPendingIntentCreatorBackgroundActivityStartMode(
+ @BackgroundActivityStartMode int mode) {
+ mPendingIntentCreatorBackgroundActivityStartMode = mode;
+ return this;
+ }
+
+ /**
+ * Returns the mode to start background activities granted by the creator of the
+ * {@link PendingIntent}.
+ *
+ * @return the {@link android.app.ComponentOptions.BackgroundActivityStartMode} currently set
+ */
+ public @BackgroundActivityStartMode int getPendingIntentCreatorBackgroundActivityStartMode() {
+ return mPendingIntentCreatorBackgroundActivityStartMode;
}
/**
@@ -2295,9 +2317,10 @@
if (mDismissKeyguard) {
b.putBoolean(KEY_DISMISS_KEYGUARD, mDismissKeyguard);
}
- if (mIgnorePendingIntentCreatorForegroundState) {
- b.putBoolean(KEY_IGNORE_PENDING_INTENT_CREATOR_FOREGROUND_STATE,
- mIgnorePendingIntentCreatorForegroundState);
+ if (mPendingIntentCreatorBackgroundActivityStartMode
+ != MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED) {
+ b.putInt(KEY_PENDING_INTENT_CREATOR_BACKGROUND_ACTIVITY_START_MODE,
+ mPendingIntentCreatorBackgroundActivityStartMode);
}
if (mDisableStartingWindow) {
b.putBoolean(KEY_DISABLE_STARTING_WINDOW, mDisableStartingWindow);
diff --git a/core/java/android/hardware/fingerprint/IUdfpsRefreshRateRequestCallback.aidl b/core/java/android/hardware/fingerprint/IUdfpsRefreshRateRequestCallback.aidl
index 8587348..7844b40 100644
--- a/core/java/android/hardware/fingerprint/IUdfpsRefreshRateRequestCallback.aidl
+++ b/core/java/android/hardware/fingerprint/IUdfpsRefreshRateRequestCallback.aidl
@@ -39,5 +39,15 @@
* {@link android.view.Display#getDisplayId()}.
*/
void onRequestDisabled(int displayId);
+
+ /**
+ * To avoid delay in switching refresh rate when activating LHBM, allow screens to request
+ * higher refersh rate if auth is possible on particular screen
+ *
+ * @param displayId The displayId for which the refresh rate should be unset. See
+ * {@link android.view.Display#getDisplayId()}.
+ * @param isPossible If authentication is possible on particualr screen
+ */
+ void onAuthenticationPossible(int displayId, boolean isPossible);
}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 1c032ee..b29efab 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -11468,6 +11468,13 @@
"extra_automatic_power_save_mode";
/**
+ * Whether lockscreen weather is enabled.
+ *
+ * @hide
+ */
+ public static final String LOCK_SCREEN_WEATHER_ENABLED = "lockscreen_weather_enabled";
+
+ /**
* These entries are considered common between the personal and the managed profile,
* since the managed profile doesn't get to change them.
*/
diff --git a/core/java/android/service/credentials/GetCredentialRequest.java b/core/java/android/service/credentials/GetCredentialRequest.java
index e808ace..7cdccc6 100644
--- a/core/java/android/service/credentials/GetCredentialRequest.java
+++ b/core/java/android/service/credentials/GetCredentialRequest.java
@@ -90,7 +90,7 @@
* Returns the parameters needed to return a given type of credential.
*/
@NonNull
- public CredentialOption getGetCredentialOption() {
+ public CredentialOption getCredentialOption() {
return mCredentialOption;
}
}
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 92a9e56..82de7b8 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2193,6 +2193,9 @@
<!-- The name of the package that will hold the system financed device controller role. -->
<string name="config_systemFinancedDeviceController" translatable="false">com.android.devicelockcontroller</string>
+ <!-- The component name of the wear service class that will be started by the system server. -->
+ <string name="config_wearServiceComponent" translatable="false"></string>
+
<!-- The name of the package that will handle updating the device management role. -->
<string name="config_devicePolicyManagementUpdater" translatable="false"></string>
@@ -6207,6 +6210,18 @@
different from the home screen wallpaper. -->
<bool name="config_independentLockscreenLiveWallpaper">false</bool>
+ <!-- Device state that corresponds to concurrent display mode where the default display
+ is the internal display. Public API for the feature is provided through Jetpack
+ WindowManager.
+ TODO(b/236022708) Move concurrent display state to device state config file
+ -->
+ <integer name="config_deviceStateConcurrentRearDisplay">-1</integer>
+
+ <!-- Physical display address that corresponds to the rear display in rear display mode
+ and concurrent display mode. Used to get information about the display before
+ entering the corresponding modes -->
+ <string name="config_rearDisplayPhysicalAddress" translatable="false"></string>
+
<!-- List of certificate to be used for font fs-verity integrity verification -->
<string-array translatable="false" name="config_fontManagerServiceCerts">
</string-array>
@@ -6276,4 +6291,7 @@
"stopped" during initial boot of a device, or after an OTA update. Stopped state of
an app is not changed during subsequent reboots. -->
<bool name="config_stopSystemPackagesByDefault">false</bool>
+
+ <!-- Whether to show weather on the lock screen by default. -->
+ <bool name="config_lockscreenWeatherEnabledByDefault">false</bool>
</resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index f460219a..aeb46cc 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4907,6 +4907,8 @@
<java-symbol type="string" name="concurrent_display_notification_thermal_content"/>
<java-symbol type="string" name="device_state_notification_turn_off_button"/>
<java-symbol type="bool" name="config_independentLockscreenLiveWallpaper"/>
+ <java-symbol type="integer" name="config_deviceStateConcurrentRearDisplay" />
+ <java-symbol type="string" name="config_rearDisplayPhysicalAddress" />
<!-- For app language picker -->
<java-symbol type="string" name="system_locale_title" />
@@ -4926,4 +4928,8 @@
<java-symbol type="array" name="config_displayShapeArray" />
<java-symbol type="bool" name="config_stopSystemPackagesByDefault"/>
+ <java-symbol type="string" name="config_wearServiceComponent" />
+
+ <!-- Whether to show weather on the lockscreen by default. -->
+ <java-symbol type="bool" name="config_lockscreenWeatherEnabledByDefault" />
</resources>
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index f2d6250..53f4747 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -61,6 +61,12 @@
"group": "WM_DEBUG_APP_TRANSITIONS",
"at": "com\/android\/server\/wm\/WindowToken.java"
},
+ "-2088209279": {
+ "message": "Notified TransitionController that the display is ready.",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_WINDOW_TRANSITIONS",
+ "at": "com\/android\/server\/wm\/WindowManagerService.java"
+ },
"-2072089308": {
"message": "Attempted to add window with token that is a sub-window: %s. Aborting.",
"level": "WARN",
@@ -2767,6 +2773,12 @@
"group": "WM_DEBUG_STATES",
"at": "com\/android\/server\/wm\/TaskFragment.java"
},
+ "378890013": {
+ "message": "Apply and finish immediately because player is disabled for transition #%d .",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_WINDOW_TRANSITIONS",
+ "at": "com\/android\/server\/wm\/Transition.java"
+ },
"385237117": {
"message": "moveFocusableActivityToTop: already on top and focused, activity=%s",
"level": "DEBUG",
@@ -3649,6 +3661,12 @@
"group": "WM_DEBUG_STATES",
"at": "com\/android\/server\/wm\/ActivityRecord.java"
},
+ "1282992082": {
+ "message": "Disabling player for transition #%d because display isn't enabled yet",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_WINDOW_TRANSITIONS",
+ "at": "com\/android\/server\/wm\/TransitionController.java"
+ },
"1284122013": {
"message": "TaskFragment appeared name=%s",
"level": "VERBOSE",
diff --git a/errorprone/java/com/google/errorprone/bugpatterns/android/RequiresPermissionChecker.java b/errorprone/java/com/google/errorprone/bugpatterns/android/RequiresPermissionChecker.java
index fa173072..d39d4b4 100644
--- a/errorprone/java/com/google/errorprone/bugpatterns/android/RequiresPermissionChecker.java
+++ b/errorprone/java/com/google/errorprone/bugpatterns/android/RequiresPermissionChecker.java
@@ -412,11 +412,11 @@
private static ParsedRequiresPermission parseRequiresPermissionRecursively(
MethodInvocationTree tree, VisitorState state) {
- if (ENFORCE_VIA_CONTEXT.matches(tree, state)) {
+ if (ENFORCE_VIA_CONTEXT.matches(tree, state) && tree.getArguments().size() > 0) {
final ParsedRequiresPermission res = new ParsedRequiresPermission();
res.allOf.add(String.valueOf(ASTHelpers.constValue(tree.getArguments().get(0))));
return res;
- } else if (ENFORCE_VIA_CHECKER.matches(tree, state)) {
+ } else if (ENFORCE_VIA_CHECKER.matches(tree, state) && tree.getArguments().size() > 1) {
final ParsedRequiresPermission res = new ParsedRequiresPermission();
res.allOf.add(String.valueOf(ASTHelpers.constValue(tree.getArguments().get(1))));
return res;
diff --git a/errorprone/tests/java/com/google/errorprone/bugpatterns/android/RequiresPermissionCheckerTest.java b/errorprone/tests/java/com/google/errorprone/bugpatterns/android/RequiresPermissionCheckerTest.java
index 388988e..38831b1 100644
--- a/errorprone/tests/java/com/google/errorprone/bugpatterns/android/RequiresPermissionCheckerTest.java
+++ b/errorprone/tests/java/com/google/errorprone/bugpatterns/android/RequiresPermissionCheckerTest.java
@@ -415,4 +415,27 @@
"}")
.doTest();
}
+
+ @Test
+ public void testInvalidFunctions() {
+ compilationHelper
+ .addSourceFile("/android/annotation/RequiresPermission.java")
+ .addSourceFile("/android/annotation/SuppressLint.java")
+ .addSourceFile("/android/content/Context.java")
+ .addSourceLines("Example.java",
+ "import android.annotation.RequiresPermission;",
+ "import android.annotation.SuppressLint;",
+ "import android.content.Context;",
+ "class Foo extends Context {",
+ " private static final String RED = \"red\";",
+ " public void checkPermission() {",
+ " }",
+ " @RequiresPermission(RED)",
+ " // BUG: Diagnostic contains:",
+ " public void exampleScoped(Context context) {",
+ " checkPermission();",
+ " }",
+ "}")
+ .doTest();
+ }
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentation.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentation.java
new file mode 100644
index 0000000..1ff1694
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentation.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2023 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 androidx.window.extensions.area;
+
+import android.app.Presentation;
+import android.content.Context;
+import android.view.Display;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.window.extensions.core.util.function.Consumer;
+
+/**
+ * {@link Presentation} object that is used to present extra content
+ * on the rear facing display when in a rear display presentation feature.
+ */
+class RearDisplayPresentation extends Presentation implements ExtensionWindowAreaPresentation {
+
+ @NonNull
+ private final Consumer<@WindowAreaComponent.WindowAreaSessionState Integer> mStateConsumer;
+
+ RearDisplayPresentation(@NonNull Context outerContext, @NonNull Display display,
+ @NonNull Consumer<@WindowAreaComponent.WindowAreaSessionState Integer> stateConsumer) {
+ super(outerContext, display);
+ mStateConsumer = stateConsumer;
+ }
+
+ /**
+ * {@code mStateConsumer} is notified that their content is now visible when the
+ * {@link Presentation} object is started. There is no comparable callback for
+ * {@link WindowAreaComponent#SESSION_STATE_INVISIBLE} in {@link #onStop()} due to the
+ * timing of when a {@link android.hardware.devicestate.DeviceStateRequest} is cancelled
+ * ending rear display presentation mode happening before the {@link Presentation} is stopped.
+ */
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mStateConsumer.accept(WindowAreaComponent.SESSION_STATE_VISIBLE);
+ }
+
+ @NonNull
+ @Override
+ public Context getPresentationContext() {
+ return getContext();
+ }
+
+ @Override
+ public void setPresentationView(View view) {
+ setContentView(view);
+ if (!isShowing()) {
+ show();
+ }
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentationController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentationController.java
new file mode 100644
index 0000000..141a6ad
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentationController.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2023 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 androidx.window.extensions.area;
+
+import static androidx.window.extensions.area.WindowAreaComponent.SESSION_STATE_ACTIVE;
+import static androidx.window.extensions.area.WindowAreaComponent.SESSION_STATE_INACTIVE;
+
+import android.content.Context;
+import android.hardware.devicestate.DeviceStateRequest;
+import android.hardware.display.DisplayManager;
+import android.util.Log;
+import android.view.Display;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.window.extensions.core.util.function.Consumer;
+
+import java.util.Objects;
+
+/**
+ * Controller class that keeps track of the status of the device state request
+ * to enable the rear display presentation feature. This controller notifies the session callback
+ * when the state request is active, and notifies the callback when the request is canceled.
+ *
+ * Clients are notified via {@link Consumer} provided with
+ * {@link androidx.window.extensions.area.WindowAreaComponent.WindowAreaStatus} values to signify
+ * when the request becomes active and cancelled.
+ */
+class RearDisplayPresentationController implements DeviceStateRequest.Callback {
+
+ private static final String TAG = "RearDisplayPresentationController";
+
+ // Original context that requested to enable rear display presentation mode
+ @NonNull
+ private final Context mContext;
+ @NonNull
+ private final Consumer<@WindowAreaComponent.WindowAreaSessionState Integer> mStateConsumer;
+ @Nullable
+ private ExtensionWindowAreaPresentation mExtensionWindowAreaPresentation;
+ @NonNull
+ private final DisplayManager mDisplayManager;
+
+ /**
+ * Creates the RearDisplayPresentationController
+ * @param context Originating {@link android.content.Context} that is initiating the rear
+ * display presentation session.
+ * @param stateConsumer {@link Consumer} that will be notified that the session is active when
+ * the device state request is active and the session has been created. If the device
+ * state request is cancelled, the callback will be notified that the session has been
+ * ended. This could occur through a call to cancel the feature or if the device is
+ * manipulated in a way that cancels any device state override.
+ */
+ RearDisplayPresentationController(@NonNull Context context,
+ @NonNull Consumer<@WindowAreaComponent.WindowAreaSessionState Integer> stateConsumer) {
+ Objects.requireNonNull(context);
+ Objects.requireNonNull(stateConsumer);
+
+ mContext = context;
+ mStateConsumer = stateConsumer;
+ mDisplayManager = context.getSystemService(DisplayManager.class);
+ }
+
+ @Override
+ public void onRequestActivated(@NonNull DeviceStateRequest request) {
+ Display[] rearDisplays = mDisplayManager.getDisplays(DisplayManager.DISPLAY_CATEGORY_REAR);
+ if (rearDisplays.length == 0) {
+ mStateConsumer.accept(SESSION_STATE_INACTIVE);
+ Log.e(TAG, "Rear display list should not be empty");
+ return;
+ }
+
+ mExtensionWindowAreaPresentation =
+ new RearDisplayPresentation(mContext, rearDisplays[0], mStateConsumer);
+ mStateConsumer.accept(SESSION_STATE_ACTIVE);
+ }
+
+ @Override
+ public void onRequestCanceled(@NonNull DeviceStateRequest request) {
+ mStateConsumer.accept(SESSION_STATE_INACTIVE);
+ }
+
+ @Nullable
+ public ExtensionWindowAreaPresentation getWindowAreaPresentation() {
+ return mExtensionWindowAreaPresentation;
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentationStatus.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentationStatus.java
new file mode 100644
index 0000000..0b1423a
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentationStatus.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2023 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 androidx.window.extensions.area;
+
+import android.util.DisplayMetrics;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Class that provides information around the current status of a window area feature. Contains
+ * the current {@link WindowAreaComponent.WindowAreaStatus} value corresponding to the
+ * rear display presentation feature, as well as the {@link DisplayMetrics} for the rear facing
+ * display.
+ */
+class RearDisplayPresentationStatus implements ExtensionWindowAreaStatus {
+
+ @WindowAreaComponent.WindowAreaStatus
+ private final int mWindowAreaStatus;
+
+ @NonNull
+ private final DisplayMetrics mDisplayMetrics;
+
+ RearDisplayPresentationStatus(@WindowAreaComponent.WindowAreaStatus int status,
+ @NonNull DisplayMetrics displayMetrics) {
+ mWindowAreaStatus = status;
+ mDisplayMetrics = displayMetrics;
+ }
+
+ /**
+ * Returns the {@link androidx.window.extensions.area.WindowAreaComponent.WindowAreaStatus}
+ * value that relates to the current status of a feature.
+ */
+ @Override
+ @WindowAreaComponent.WindowAreaStatus
+ public int getWindowAreaStatus() {
+ return mWindowAreaStatus;
+ }
+
+ /**
+ * Returns the {@link DisplayMetrics} that corresponds to the window area that a feature
+ * interacts with. This is converted to size class information provided to developers.
+ */
+ @Override
+ @NonNull
+ public DisplayMetrics getWindowAreaDisplayMetrics() {
+ return mDisplayMetrics;
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
index 20602a1..274dcae 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
@@ -22,7 +22,12 @@
import android.content.Context;
import android.hardware.devicestate.DeviceStateManager;
import android.hardware.devicestate.DeviceStateRequest;
+import android.hardware.display.DisplayManager;
import android.util.ArraySet;
+import android.util.DisplayMetrics;
+import android.util.Pair;
+import android.view.Display;
+import android.view.DisplayAddress;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -30,6 +35,7 @@
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.ArrayUtils;
import java.util.concurrent.Executor;
@@ -47,61 +53,102 @@
private final Object mLock = new Object();
+ @NonNull
private final DeviceStateManager mDeviceStateManager;
+ @NonNull
+ private final DisplayManager mDisplayManager;
+ @NonNull
private final Executor mExecutor;
@GuardedBy("mLock")
private final ArraySet<Consumer<Integer>> mRearDisplayStatusListeners = new ArraySet<>();
+ @GuardedBy("mLock")
+ private final ArraySet<Consumer<ExtensionWindowAreaStatus>>
+ mRearDisplayPresentationStatusListeners = new ArraySet<>();
private final int mRearDisplayState;
+ private final int mConcurrentDisplayState;
+ @NonNull
+ private final int[] mFoldedDeviceStates;
+ @NonNull
+ private long mRearDisplayAddress = 0;
@WindowAreaSessionState
private int mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_INACTIVE;
@GuardedBy("mLock")
private int mCurrentDeviceState = INVALID_DEVICE_STATE;
@GuardedBy("mLock")
- private int mCurrentDeviceBaseState = INVALID_DEVICE_STATE;
+ private int[] mCurrentSupportedDeviceStates;
+
@GuardedBy("mLock")
- private DeviceStateRequest mDeviceStateRequest;
+ private DeviceStateRequest mRearDisplayStateRequest;
+ @GuardedBy("mLock")
+ private RearDisplayPresentationController mRearDisplayPresentationController;
+
+ @Nullable
+ @GuardedBy("mLock")
+ private DisplayMetrics mRearDisplayMetrics;
+
+ @WindowAreaSessionState
+ @GuardedBy("mLock")
+ private int mLastReportedRearDisplayPresentationStatus;
public WindowAreaComponentImpl(@NonNull Context context) {
mDeviceStateManager = context.getSystemService(DeviceStateManager.class);
+ mDisplayManager = context.getSystemService(DisplayManager.class);
mExecutor = context.getMainExecutor();
+ mCurrentSupportedDeviceStates = mDeviceStateManager.getSupportedStates();
+ mFoldedDeviceStates = context.getResources().getIntArray(
+ R.array.config_foldedDeviceStates);
+
// TODO(b/236022708) Move rear display state to device state config file
mRearDisplayState = context.getResources().getInteger(
R.integer.config_deviceStateRearDisplay);
+ mConcurrentDisplayState = context.getResources().getInteger(
+ R.integer.config_deviceStateConcurrentRearDisplay);
+
mDeviceStateManager.registerCallback(mExecutor, this);
+ if (mConcurrentDisplayState != INVALID_DEVICE_STATE) {
+ mRearDisplayAddress = Long.parseLong(context.getResources().getString(
+ R.string.config_rearDisplayPhysicalAddress));
+ }
}
/**
* Adds a listener interested in receiving updates on the RearDisplayStatus
* of the device. Because this is being called from the OEM provided
- * extensions, we will post the result of the listener on the executor
+ * extensions, the result of the listener will be posted on the executor
* provided by the developer at the initial call site.
*
- * Depending on the initial state of the device, we will return either
+ * Rear display mode moves the calling application to the display on the device that is
+ * facing the same direction as the rear cameras. This would be the cover display on a fold-in
+ * style device when the device is opened.
+ *
+ * Depending on the initial state of the device, the {@link Consumer} will receive either
* {@link WindowAreaComponent#STATUS_AVAILABLE} or
* {@link WindowAreaComponent#STATUS_UNAVAILABLE} if the feature is supported or not in that
- * state respectively. When the rear display feature is triggered, we update the status to be
- * {@link WindowAreaComponent#STATUS_UNAVAILABLE}. TODO(b/240727590) Prefix with AREA_
+ * state respectively. When the rear display feature is triggered, the status is updated to be
+ * {@link WindowAreaComponent#STATUS_UNAVAILABLE}.
+ * TODO(b/240727590): Prefix with AREA_
*
- * TODO(b/239833099) Add a STATUS_ACTIVE option to let apps know if a feature is currently
- * enabled.
+ * TODO(b/239833099): Add a STATUS_ACTIVE option to let apps know if a feature is currently
+ * enabled.
*
* @param consumer {@link Consumer} interested in receiving updates to the status of
* rear display mode.
*/
+ @Override
public void addRearDisplayStatusListener(
@NonNull Consumer<@WindowAreaStatus Integer> consumer) {
synchronized (mLock) {
mRearDisplayStatusListeners.add(consumer);
- // If current device state is still invalid, we haven't gotten our initial value yet
+ // If current device state is still invalid, the initial value has not been provided.
if (mCurrentDeviceState == INVALID_DEVICE_STATE) {
return;
}
- consumer.accept(getCurrentStatus());
+ consumer.accept(getCurrentRearDisplayModeStatus());
}
}
@@ -109,6 +156,7 @@
* Removes a listener no longer interested in receiving updates.
* @param consumer no longer interested in receiving updates to RearDisplayStatus
*/
+ @Override
public void removeRearDisplayStatusListener(
@NonNull Consumer<@WindowAreaStatus Integer> consumer) {
synchronized (mLock) {
@@ -119,13 +167,17 @@
/**
* Creates and starts a rear display session and provides updates to the
* callback provided. Because this is being called from the OEM provided
- * extensions, we will post the result of the listener on the executor
+ * extensions, the result of the listener will be posted on the executor
* provided by the developer at the initial call site.
*
- * When we enable rear display mode, we submit a request to {@link DeviceStateManager}
+ * Rear display mode moves the calling application to the display on the device that is
+ * facing the same direction as the rear cameras. This would be the cover display on a fold-in
+ * style device when the device is opened.
+ *
+ * When rear display mode is enabled, a request is made to {@link DeviceStateManager}
* to override the device state to the state that corresponds to RearDisplay
- * mode. When the {@link DeviceStateRequest} is activated, we let the
- * consumer know that the session is active by sending
+ * mode. When the {@link DeviceStateRequest} is activated, the provided {@link Consumer} is
+ * notified that the session is active by receiving
* {@link WindowAreaComponent#SESSION_STATE_ACTIVE}.
*
* @param activity to provide updates to the client on
@@ -133,19 +185,20 @@
* @param rearDisplaySessionCallback to provide updates to the client on
* the status of the Session
*/
+ @Override
public void startRearDisplaySession(@NonNull Activity activity,
@NonNull Consumer<@WindowAreaSessionState Integer> rearDisplaySessionCallback) {
synchronized (mLock) {
- if (mDeviceStateRequest != null) {
+ if (mRearDisplayStateRequest != null) {
// Rear display session is already active
throw new IllegalStateException(
"Unable to start new rear display session as one is already active");
}
- mDeviceStateRequest = DeviceStateRequest.newBuilder(mRearDisplayState).build();
+ mRearDisplayStateRequest = DeviceStateRequest.newBuilder(mRearDisplayState).build();
mDeviceStateManager.requestState(
- mDeviceStateRequest,
+ mRearDisplayStateRequest,
mExecutor,
- new DeviceStateRequestCallbackAdapter(rearDisplaySessionCallback)
+ new RearDisplayStateRequestCallbackAdapter(rearDisplaySessionCallback)
);
}
}
@@ -153,13 +206,14 @@
/**
* Ends the current rear display session and provides updates to the
* callback provided. Because this is being called from the OEM provided
- * extensions, we will post the result of the listener on the executor
- * provided by the developer.
+ * extensions, the result of the listener will be posted on the executor
+ * provided by the developer at the initial call site.
*/
+ @Override
public void endRearDisplaySession() {
synchronized (mLock) {
- if (mDeviceStateRequest != null || isRearDisplayActive()) {
- mDeviceStateRequest = null;
+ if (mRearDisplayStateRequest != null || isRearDisplayActive()) {
+ mRearDisplayStateRequest = null;
mDeviceStateManager.cancelStateRequest();
} else {
throw new IllegalStateException(
@@ -168,13 +222,176 @@
}
}
+ /**
+ * Adds a listener interested in receiving updates on the RearDisplayPresentationStatus
+ * of the device. Because this is being called from the OEM provided
+ * extensions, the result of the listener will be posted on the executor
+ * provided by the developer at the initial call site.
+ *
+ * Rear display presentation mode is a feature where an {@link Activity} can present
+ * additional content on a device with a second display that is facing the same direction
+ * as the rear camera (i.e. the cover display on a fold-in style device). The calling
+ * {@link Activity} does not move, whereas in rear display mode it does.
+ *
+ * This listener receives a {@link Pair} with the first item being the
+ * {@link WindowAreaComponent.WindowAreaStatus} that corresponds to the current status of the
+ * feature, and the second being the {@link DisplayMetrics} of the display that would be
+ * presented to when the feature is active.
+ *
+ * Depending on the initial state of the device, the {@link Consumer} will receive either
+ * {@link WindowAreaComponent#STATUS_AVAILABLE} or
+ * {@link WindowAreaComponent#STATUS_UNAVAILABLE} for the status value of the {@link Pair} if
+ * the feature is supported or not in that state respectively. Rear display presentation mode is
+ * currently not supported when the device is folded. When the rear display presentation feature
+ * is triggered, the status is updated to be {@link WindowAreaComponent#STATUS_UNAVAILABLE}.
+ * TODO(b/240727590): Prefix with AREA_
+ *
+ * TODO(b/239833099): Add a STATUS_ACTIVE option to let apps know if a feature is currently
+ * enabled.
+ *
+ * @param consumer {@link Consumer} interested in receiving updates to the status of
+ * rear display presentation mode.
+ */
@Override
- public void onBaseStateChanged(int state) {
+ public void addRearDisplayPresentationStatusListener(
+ @NonNull Consumer<ExtensionWindowAreaStatus> consumer) {
synchronized (mLock) {
- mCurrentDeviceBaseState = state;
- if (state == mCurrentDeviceState) {
- updateStatusConsumers(getCurrentStatus());
+ mRearDisplayPresentationStatusListeners.add(consumer);
+
+ // If current device state is still invalid, the initial value has not been provided
+ if (mCurrentDeviceState == INVALID_DEVICE_STATE) {
+ return;
}
+ @WindowAreaStatus int currentStatus = getCurrentRearDisplayPresentationModeStatus();
+ consumer.accept(
+ new RearDisplayPresentationStatus(currentStatus, getRearDisplayMetrics()));
+ }
+ }
+
+ /**
+ * Removes a listener no longer interested in receiving updates.
+ * @param consumer no longer interested in receiving updates to RearDisplayPresentationStatus
+ */
+ @Override
+ public void removeRearDisplayPresentationStatusListener(
+ @NonNull Consumer<ExtensionWindowAreaStatus> consumer) {
+ synchronized (mLock) {
+ mRearDisplayPresentationStatusListeners.remove(consumer);
+ }
+ }
+
+ /**
+ * Creates and starts a rear display presentation session and sends state updates to the
+ * consumer provided. This consumer will receive a constant represented by
+ * {@link WindowAreaSessionState} to represent the state of the current rear display
+ * session. It will be translated to a more friendly interface in the library.
+ *
+ * Because this is being called from the OEM provided extensions, the library
+ * will post the result of the listener on the executor provided by the developer.
+ *
+ * Rear display presentation mode refers to a feature where an {@link Activity} can present
+ * additional content on a device with a second display that is facing the same direction
+ * as the rear camera (i.e. the cover display on a fold-in style device). The calling
+ * {@link Activity} stays on the user-facing display.
+ *
+ * @param activity that the OEM implementation will use as a base
+ * context and to identify the source display area of the request.
+ * The reference to the activity instance must not be stored in the OEM
+ * implementation to prevent memory leaks.
+ * @param consumer to provide updates to the client on the status of the session
+ * @throws UnsupportedOperationException if this method is called when rear display presentation
+ * mode is not available. This could be to an incompatible device state or when
+ * another process is currently in this mode.
+ */
+ @Override
+ public void startRearDisplayPresentationSession(@NonNull Activity activity,
+ @NonNull Consumer<@WindowAreaSessionState Integer> consumer) {
+ synchronized (mLock) {
+ if (mRearDisplayPresentationController != null) {
+ // Rear display presentation session is already active
+ throw new IllegalStateException(
+ "Unable to start new rear display presentation session as one is already "
+ + "active");
+ }
+ if (getCurrentRearDisplayPresentationModeStatus()
+ != WindowAreaComponent.STATUS_AVAILABLE) {
+ throw new IllegalStateException(
+ "Unable to start new rear display presentation session as the feature is "
+ + "is not currently available");
+ }
+
+ mRearDisplayPresentationController = new RearDisplayPresentationController(activity,
+ stateStatus -> {
+ synchronized (mLock) {
+ if (stateStatus == SESSION_STATE_INACTIVE) {
+ // If the last reported session status was VISIBLE
+ // then the INVISIBLE state should be dispatched before INACTIVE
+ // due to not having a good mechanism to know when
+ // the content is no longer visible before it's fully removed
+ if (getLastReportedRearDisplayPresentationStatus()
+ == SESSION_STATE_VISIBLE) {
+ consumer.accept(SESSION_STATE_INVISIBLE);
+ }
+ mRearDisplayPresentationController = null;
+ }
+ mLastReportedRearDisplayPresentationStatus = stateStatus;
+ consumer.accept(stateStatus);
+ }
+ });
+
+ DeviceStateRequest concurrentDisplayStateRequest = DeviceStateRequest.newBuilder(
+ mConcurrentDisplayState).build();
+ mDeviceStateManager.requestState(
+ concurrentDisplayStateRequest,
+ mExecutor,
+ mRearDisplayPresentationController
+ );
+ }
+ }
+
+ /**
+ * Ends the current rear display presentation session and provides updates to the
+ * callback provided. When this is ended, the presented content from the calling
+ * {@link Activity} will also be removed from the rear facing display.
+ * Because this is being called from the OEM provided extensions, the result of the listener
+ * will be posted on the executor provided by the developer at the initial call site.
+ *
+ * Cancelling the {@link DeviceStateRequest} and exiting the rear display presentation state,
+ * will remove the presentation window from the cover display as the cover display is no longer
+ * enabled.
+ */
+ @Override
+ public void endRearDisplayPresentationSession() {
+ synchronized (mLock) {
+ if (mRearDisplayPresentationController != null) {
+ mDeviceStateManager.cancelStateRequest();
+ } else {
+ throw new IllegalStateException(
+ "Unable to cancel a rear display presentation session as there is no "
+ + "active session");
+ }
+ }
+ }
+
+ @Nullable
+ @Override
+ public ExtensionWindowAreaPresentation getRearDisplayPresentation() {
+ synchronized (mLock) {
+ ExtensionWindowAreaPresentation presentation = null;
+ if (mRearDisplayPresentationController != null) {
+ presentation = mRearDisplayPresentationController.getWindowAreaPresentation();
+ }
+ return presentation;
+ }
+ }
+
+ @Override
+ public void onSupportedStatesChanged(int[] supportedStates) {
+ synchronized (mLock) {
+ mCurrentSupportedDeviceStates = supportedStates;
+ updateRearDisplayStatusListeners(getCurrentRearDisplayModeStatus());
+ updateRearDisplayPresentationStatusListeners(
+ getCurrentRearDisplayPresentationModeStatus());
}
}
@@ -182,34 +399,17 @@
public void onStateChanged(int state) {
synchronized (mLock) {
mCurrentDeviceState = state;
- updateStatusConsumers(getCurrentStatus());
+ updateRearDisplayStatusListeners(getCurrentRearDisplayModeStatus());
+ updateRearDisplayPresentationStatusListeners(
+ getCurrentRearDisplayPresentationModeStatus());
}
}
- @Override
- public void addRearDisplayPresentationStatusListener(
- @NonNull Consumer<ExtensionWindowAreaStatus> consumer) {}
-
- @Override
- public void removeRearDisplayPresentationStatusListener(
- @NonNull Consumer<ExtensionWindowAreaStatus> consumer) {}
-
- @Override
- public void startRearDisplayPresentationSession(@NonNull Activity activity,
- @NonNull Consumer<@WindowAreaSessionState Integer> consumer) {}
-
- @Override
- public void endRearDisplayPresentationSession() {}
-
- @Override
- @Nullable
- public ExtensionWindowAreaPresentation getRearDisplayPresentation() {
- return null;
- }
@GuardedBy("mLock")
- private int getCurrentStatus() {
+ private int getCurrentRearDisplayModeStatus() {
if (mRearDisplaySessionStatus == WindowAreaComponent.SESSION_STATE_ACTIVE
+ || !ArrayUtils.contains(mCurrentSupportedDeviceStates, mRearDisplayState)
|| isRearDisplayActive()) {
return WindowAreaComponent.STATUS_UNAVAILABLE;
}
@@ -218,19 +418,20 @@
/**
* Helper method to determine if a rear display session is currently active by checking
- * if the current device configuration matches that of rear display. This would be true
- * if there is a device override currently active (base state != current state) and the current
- * state is that which corresponds to {@code mRearDisplayState}
- * @return {@code true} if the device is in rear display mode and {@code false} if not
+ * if the current device state is that which corresponds to {@code mRearDisplayState}.
+ *
+ * @return {@code true} if the device is in rear display state {@code false} if not
*/
@GuardedBy("mLock")
private boolean isRearDisplayActive() {
- return (mCurrentDeviceState != mCurrentDeviceBaseState) && (mCurrentDeviceState
- == mRearDisplayState);
+ return mCurrentDeviceState == mRearDisplayState;
}
@GuardedBy("mLock")
- private void updateStatusConsumers(@WindowAreaStatus int windowAreaStatus) {
+ private void updateRearDisplayStatusListeners(@WindowAreaStatus int windowAreaStatus) {
+ if (mRearDisplayState == INVALID_DEVICE_STATE) {
+ return;
+ }
synchronized (mLock) {
for (int i = 0; i < mRearDisplayStatusListeners.size(); i++) {
mRearDisplayStatusListeners.valueAt(i).accept(windowAreaStatus);
@@ -238,26 +439,95 @@
}
}
+ @GuardedBy("mLock")
+ private int getCurrentRearDisplayPresentationModeStatus() {
+ if (mCurrentDeviceState == mConcurrentDisplayState
+ || !ArrayUtils.contains(mCurrentSupportedDeviceStates, mConcurrentDisplayState)
+ || isDeviceFolded()) {
+ return WindowAreaComponent.STATUS_UNAVAILABLE;
+ }
+ return WindowAreaComponent.STATUS_AVAILABLE;
+ }
+
+ @GuardedBy("mLock")
+ private boolean isDeviceFolded() {
+ return ArrayUtils.contains(mFoldedDeviceStates, mCurrentDeviceState);
+ }
+
+ @GuardedBy("mLock")
+ private void updateRearDisplayPresentationStatusListeners(
+ @WindowAreaStatus int windowAreaStatus) {
+ if (mConcurrentDisplayState == INVALID_DEVICE_STATE) {
+ return;
+ }
+ RearDisplayPresentationStatus consumerValue = new RearDisplayPresentationStatus(
+ windowAreaStatus, getRearDisplayMetrics());
+ synchronized (mLock) {
+ for (int i = 0; i < mRearDisplayPresentationStatusListeners.size(); i++) {
+ mRearDisplayPresentationStatusListeners.valueAt(i).accept(consumerValue);
+ }
+ }
+ }
+
+ /**
+ * Returns the{@link DisplayMetrics} associated with the rear facing display. If the rear facing
+ * display was not found in the display list, but we have already computed the
+ * {@link DisplayMetrics} for that display, we return the cached value.
+ *
+ * TODO(b/267563768): Update with guidance from Display team for missing displays.
+ *
+ * @throws IllegalArgumentException if the display is not found and there is no cached
+ * {@link DisplayMetrics} for this display.
+ */
+ @GuardedBy("mLock")
+ private DisplayMetrics getRearDisplayMetrics() {
+ Display[] displays = mDisplayManager.getDisplays(
+ DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED);
+ for (int i = 0; i < displays.length; i++) {
+ DisplayAddress.Physical address =
+ (DisplayAddress.Physical) displays[i].getAddress();
+ if (mRearDisplayAddress == address.getPhysicalDisplayId()) {
+ if (mRearDisplayMetrics == null) {
+ mRearDisplayMetrics = new DisplayMetrics();
+ }
+ displays[i].getRealMetrics(mRearDisplayMetrics);
+ return mRearDisplayMetrics;
+ }
+ }
+ if (mRearDisplayMetrics != null) {
+ return mRearDisplayMetrics;
+ } else {
+ throw new IllegalArgumentException(
+ "No display found with the provided display address");
+ }
+ }
+
+ @GuardedBy("mLock")
+ @WindowAreaSessionState
+ private int getLastReportedRearDisplayPresentationStatus() {
+ return mLastReportedRearDisplayPresentationStatus;
+ }
+
/**
* Callback for the {@link DeviceStateRequest} to be notified of when the request has been
* activated or cancelled. This callback provides information to the client library
* on the status of the RearDisplay session through {@code mRearDisplaySessionCallback}
*/
- private class DeviceStateRequestCallbackAdapter implements DeviceStateRequest.Callback {
+ private class RearDisplayStateRequestCallbackAdapter implements DeviceStateRequest.Callback {
private final Consumer<Integer> mRearDisplaySessionCallback;
- DeviceStateRequestCallbackAdapter(@NonNull Consumer<Integer> callback) {
+ RearDisplayStateRequestCallbackAdapter(@NonNull Consumer<Integer> callback) {
mRearDisplaySessionCallback = callback;
}
@Override
public void onRequestActivated(@NonNull DeviceStateRequest request) {
synchronized (mLock) {
- if (request.equals(mDeviceStateRequest)) {
+ if (request.equals(mRearDisplayStateRequest)) {
mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_ACTIVE;
mRearDisplaySessionCallback.accept(mRearDisplaySessionStatus);
- updateStatusConsumers(getCurrentStatus());
+ updateRearDisplayStatusListeners(getCurrentRearDisplayModeStatus());
}
}
}
@@ -265,12 +535,12 @@
@Override
public void onRequestCanceled(DeviceStateRequest request) {
synchronized (mLock) {
- if (request.equals(mDeviceStateRequest)) {
- mDeviceStateRequest = null;
+ if (request.equals(mRearDisplayStateRequest)) {
+ mRearDisplayStateRequest = null;
}
mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_INACTIVE;
mRearDisplaySessionCallback.accept(mRearDisplaySessionStatus);
- updateStatusConsumers(getCurrentStatus());
+ updateRearDisplayStatusListeners(getCurrentRearDisplayModeStatus());
}
}
}
diff --git a/libs/WindowManager/Jetpack/window-extensions-release.aar b/libs/WindowManager/Jetpack/window-extensions-release.aar
index 378ad81..9596c22 100644
--- a/libs/WindowManager/Jetpack/window-extensions-release.aar
+++ b/libs/WindowManager/Jetpack/window-extensions-release.aar
Binary files differ
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
index 6728c00..d9ac76e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
@@ -28,6 +28,7 @@
import android.annotation.NonNull;
import android.app.TaskInfo;
import android.content.Context;
+import android.content.pm.ActivityInfo;
import android.graphics.Rect;
import android.view.Surface;
import android.view.SurfaceControl;
@@ -361,22 +362,26 @@
}
void setColorContentOverlay(Context context) {
- final SurfaceControl.Transaction tx =
- mSurfaceControlTransactionFactory.getTransaction();
- if (mContentOverlay != null) {
- mContentOverlay.detach(tx);
- }
- mContentOverlay = new PipContentOverlay.PipColorOverlay(context);
- mContentOverlay.attach(tx, mLeash);
+ reattachContentOverlay(new PipContentOverlay.PipColorOverlay(context));
}
void setSnapshotContentOverlay(TaskSnapshot snapshot, Rect sourceRectHint) {
+ reattachContentOverlay(
+ new PipContentOverlay.PipSnapshotOverlay(snapshot, sourceRectHint));
+ }
+
+ void setAppIconContentOverlay(Context context, Rect bounds, ActivityInfo activityInfo) {
+ reattachContentOverlay(
+ new PipContentOverlay.PipAppIconOverlay(context, bounds, activityInfo));
+ }
+
+ private void reattachContentOverlay(PipContentOverlay overlay) {
final SurfaceControl.Transaction tx =
mSurfaceControlTransactionFactory.getTransaction();
if (mContentOverlay != null) {
mContentOverlay.detach(tx);
}
- mContentOverlay = new PipContentOverlay.PipSnapshotOverlay(snapshot, sourceRectHint);
+ mContentOverlay = overlay;
mContentOverlay.attach(tx, mLeash);
}
@@ -570,8 +575,9 @@
final Rect base = getBaseValue();
final Rect start = getStartValue();
final Rect end = getEndValue();
+ Rect bounds = mRectEvaluator.evaluate(fraction, start, end);
if (mContentOverlay != null) {
- mContentOverlay.onAnimationUpdate(tx, fraction);
+ mContentOverlay.onAnimationUpdate(tx, bounds, fraction);
}
if (rotatedEndRect != null) {
// Animate the bounds in a different orientation. It only happens when
@@ -579,7 +585,6 @@
applyRotation(tx, leash, fraction, start, end);
return;
}
- Rect bounds = mRectEvaluator.evaluate(fraction, start, end);
float angle = (1.0f - fraction) * startingAngle;
setCurrentValue(bounds);
if (inScaleTransition() || sourceHintRect == null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java
index 283b1ec..480bf93 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java
@@ -16,11 +16,21 @@
package com.android.wm.shell.pip;
+import static android.util.TypedValue.COMPLEX_UNIT_DIP;
+
import android.annotation.Nullable;
import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
import android.graphics.Color;
+import android.graphics.Matrix;
import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.TypedValue;
import android.view.SurfaceControl;
import android.view.SurfaceSession;
import android.window.TaskSnapshot;
@@ -51,9 +61,11 @@
* Animates the internal {@link #mLeash} by a given fraction.
* @param atomicTx {@link SurfaceControl.Transaction} to operate, you should not explicitly
* call apply on this transaction, it should be applied on the caller side.
+ * @param currentBounds {@link Rect} of the current animation bounds.
* @param fraction progress of the animation ranged from 0f to 1f.
*/
- public abstract void onAnimationUpdate(SurfaceControl.Transaction atomicTx, float fraction);
+ public abstract void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
+ Rect currentBounds, float fraction);
/**
* Callback when reaches the end of animation on the internal {@link #mLeash}.
@@ -66,13 +78,15 @@
/** A {@link PipContentOverlay} uses solid color. */
public static final class PipColorOverlay extends PipContentOverlay {
+ private static final String TAG = PipColorOverlay.class.getSimpleName();
+
private final Context mContext;
public PipColorOverlay(Context context) {
mContext = context;
mLeash = new SurfaceControl.Builder(new SurfaceSession())
- .setCallsite("PipAnimation")
- .setName(PipColorOverlay.class.getSimpleName())
+ .setCallsite(TAG)
+ .setName(TAG)
.setColorLayer()
.build();
}
@@ -88,7 +102,8 @@
}
@Override
- public void onAnimationUpdate(SurfaceControl.Transaction atomicTx, float fraction) {
+ public void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
+ Rect currentBounds, float fraction) {
atomicTx.setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2);
}
@@ -114,6 +129,8 @@
/** A {@link PipContentOverlay} uses {@link TaskSnapshot}. */
public static final class PipSnapshotOverlay extends PipContentOverlay {
+ private static final String TAG = PipSnapshotOverlay.class.getSimpleName();
+
private final TaskSnapshot mSnapshot;
private final Rect mSourceRectHint;
@@ -121,8 +138,8 @@
mSnapshot = snapshot;
mSourceRectHint = new Rect(sourceRectHint);
mLeash = new SurfaceControl.Builder(new SurfaceSession())
- .setCallsite("PipAnimation")
- .setName(PipSnapshotOverlay.class.getSimpleName())
+ .setCallsite(TAG)
+ .setName(TAG)
.build();
}
@@ -143,7 +160,8 @@
}
@Override
- public void onAnimationUpdate(SurfaceControl.Transaction atomicTx, float fraction) {
+ public void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
+ Rect currentBounds, float fraction) {
// Do nothing. Keep the snapshot till animation ends.
}
@@ -152,4 +170,113 @@
atomicTx.remove(mLeash);
}
}
+
+ /** A {@link PipContentOverlay} shows app icon on solid color background. */
+ public static final class PipAppIconOverlay extends PipContentOverlay {
+ private static final String TAG = PipAppIconOverlay.class.getSimpleName();
+ private static final int APP_ICON_SIZE_DP = 48;
+
+ private final Context mContext;
+ private final int mAppIconSizePx;
+ private final Rect mAppBounds;
+ private final Matrix mTmpTransform = new Matrix();
+ private final float[] mTmpFloat9 = new float[9];
+
+ private Bitmap mBitmap;
+
+ public PipAppIconOverlay(Context context, Rect appBounds, ActivityInfo activityInfo) {
+ mContext = context;
+ mAppIconSizePx = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, APP_ICON_SIZE_DP,
+ context.getResources().getDisplayMetrics());
+ mAppBounds = new Rect(appBounds);
+ mBitmap = Bitmap.createBitmap(appBounds.width(), appBounds.height(),
+ Bitmap.Config.ARGB_8888);
+ prepareAppIconOverlay(activityInfo);
+ mLeash = new SurfaceControl.Builder(new SurfaceSession())
+ .setCallsite(TAG)
+ .setName(TAG)
+ .build();
+ }
+
+ @Override
+ public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) {
+ tx.show(mLeash);
+ tx.setLayer(mLeash, Integer.MAX_VALUE);
+ tx.setBuffer(mLeash, mBitmap.getHardwareBuffer());
+ tx.reparent(mLeash, parentLeash);
+ tx.apply();
+ }
+
+ @Override
+ public void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
+ Rect currentBounds, float fraction) {
+ mTmpTransform.reset();
+ // Scale back the bitmap with the pivot point at center.
+ mTmpTransform.postScale(
+ (float) mAppBounds.width() / currentBounds.width(),
+ (float) mAppBounds.height() / currentBounds.height(),
+ mAppBounds.centerX(),
+ mAppBounds.centerY());
+ atomicTx.setMatrix(mLeash, mTmpTransform, mTmpFloat9)
+ .setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2);
+ }
+
+ @Override
+ public void onAnimationEnd(SurfaceControl.Transaction atomicTx, Rect destinationBounds) {
+ atomicTx.remove(mLeash);
+ }
+
+ @Override
+ public void detach(SurfaceControl.Transaction tx) {
+ super.detach(tx);
+ if (mBitmap != null && !mBitmap.isRecycled()) {
+ mBitmap.recycle();
+ }
+ }
+
+ private void prepareAppIconOverlay(ActivityInfo activityInfo) {
+ final Canvas canvas = new Canvas();
+ canvas.setBitmap(mBitmap);
+ final TypedArray ta = mContext.obtainStyledAttributes(new int[] {
+ android.R.attr.colorBackground });
+ try {
+ int colorAccent = ta.getColor(0, 0);
+ canvas.drawRGB(
+ Color.red(colorAccent),
+ Color.green(colorAccent),
+ Color.blue(colorAccent));
+ } finally {
+ ta.recycle();
+ }
+ final Drawable appIcon = loadActivityInfoIcon(activityInfo,
+ mContext.getResources().getConfiguration().densityDpi);
+ final Rect appIconBounds = new Rect(
+ mAppBounds.centerX() - mAppIconSizePx / 2,
+ mAppBounds.centerY() - mAppIconSizePx / 2,
+ mAppBounds.centerX() + mAppIconSizePx / 2,
+ mAppBounds.centerY() + mAppIconSizePx / 2);
+ appIcon.setBounds(appIconBounds);
+ appIcon.draw(canvas);
+ mBitmap = mBitmap.copy(Bitmap.Config.HARDWARE, false /* mutable */);
+ }
+
+ // Copied from com.android.launcher3.icons.IconProvider#loadActivityInfoIcon
+ private Drawable loadActivityInfoIcon(ActivityInfo ai, int density) {
+ final int iconRes = ai.getIconResource();
+ Drawable icon = null;
+ // Get the preferred density icon from the app's resources
+ if (density != 0 && iconRes != 0) {
+ try {
+ final Resources resources = mContext.getPackageManager()
+ .getResourcesForApplication(ai.applicationInfo);
+ icon = resources.getDrawableForDensity(iconRes, density);
+ } catch (PackageManager.NameNotFoundException | Resources.NotFoundException exc) { }
+ }
+ // Get the default density icon
+ if (icon == null) {
+ icon = ai.loadIcon(mContext.getPackageManager());
+ }
+ return icon;
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index 8ba2583..aad27b9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -62,6 +62,7 @@
import android.graphics.Rect;
import android.os.RemoteException;
import android.os.SystemClock;
+import android.os.SystemProperties;
import android.util.Log;
import android.view.Choreographer;
import android.view.Display;
@@ -1568,7 +1569,13 @@
// Similar to auto-enter-pip transition, we use content overlay when there is no
// source rect hint to enter PiP use bounds animation.
if (sourceHintRect == null) {
- animator.setColorContentOverlay(mContext);
+ if (SystemProperties.getBoolean(
+ "persist.wm.debug.enable_pip_app_icon_overlay", false)) {
+ animator.setAppIconContentOverlay(
+ mContext, currentBounds, mTaskInfo.topActivityInfo);
+ } else {
+ animator.setColorContentOverlay(mContext);
+ }
} else {
final TaskSnapshot snapshot = PipUtils.getTaskSnapshot(
mTaskInfo.launchIntoPipHostTaskId, false /* isLowResolution */);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index 83158ff..d9d1009 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -50,6 +50,7 @@
import android.graphics.Point;
import android.graphics.Rect;
import android.os.IBinder;
+import android.os.SystemProperties;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.WindowManager;
@@ -792,7 +793,13 @@
if (sourceHintRect == null) {
// We use content overlay when there is no source rect hint to enter PiP use bounds
// animation.
- animator.setColorContentOverlay(mContext);
+ if (SystemProperties.getBoolean(
+ "persist.wm.debug.enable_pip_app_icon_overlay", false)) {
+ animator.setAppIconContentOverlay(
+ mContext, currentBounds, taskInfo.topActivityInfo);
+ } else {
+ animator.setColorContentOverlay(mContext);
+ }
}
} else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) {
animator = mPipAnimationController.getAnimator(taskInfo, leash, destinationBounds,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 39cf5f1..9624ae9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -2108,7 +2108,7 @@
// Use normal animations.
return false;
- } else if (mMixedHandler != null && hasDisplayChange(info)) {
+ } else if (mMixedHandler != null && Transitions.hasDisplayChange(info)) {
// A display-change has been un-expectedly inserted into the transition. Redirect
// handling to the mixed-handler to deal with splitting it up.
if (mMixedHandler.animatePendingSplitWithDisplayChange(transition, info,
@@ -2151,15 +2151,6 @@
return true;
}
- private boolean hasDisplayChange(TransitionInfo info) {
- boolean has = false;
- for (int iC = 0; iC < info.getChanges().size() && !has; ++iC) {
- final TransitionInfo.Change change = info.getChanges().get(iC);
- has = change.getMode() == TRANSIT_CHANGE && (change.getFlags() & FLAG_IS_DISPLAY) != 0;
- }
- return has;
- }
-
/** Called to clean-up state and do house-keeping after the animation is done. */
public void onTransitionAnimationComplete() {
// If still playing, let it finish.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java
index b4e0584..02f19eb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java
@@ -93,6 +93,11 @@
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
+ if (!Transitions.SHELL_TRANSITIONS_ROTATION && Transitions.hasDisplayChange(info)) {
+ // Note that if the remote doesn't have permission ACCESS_SURFACE_FLINGER, some
+ // operations of the start transaction may be ignored.
+ return false;
+ }
RemoteTransition pendingRemote = mRequestedRemotes.get(transition);
if (pendingRemote == null) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition %s doesn't have "
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 44d6a0d..b2f61c2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -24,6 +24,7 @@
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static android.view.WindowManager.fixScale;
+import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
import static android.window.TransitionInfo.FLAG_IS_OCCLUDED;
import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
import static android.window.TransitionInfo.FLAG_NO_ANIMATION;
@@ -328,6 +329,17 @@
return type == TRANSIT_CLOSE || type == TRANSIT_TO_BACK;
}
+ /** Returns {@code true} if the transition has a display change. */
+ public static boolean hasDisplayChange(@NonNull TransitionInfo info) {
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ if (change.getMode() == TRANSIT_CHANGE && change.hasFlags(FLAG_IS_DISPLAY)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Sets up visibility/alpha/transforms to resemble the starting state of an animation.
*/
diff --git a/media/java/android/media/RouteListingPreference.java b/media/java/android/media/RouteListingPreference.java
index 99e9413..ab78536 100644
--- a/media/java/android/media/RouteListingPreference.java
+++ b/media/java/android/media/RouteListingPreference.java
@@ -259,7 +259,7 @@
@IntDef(
flag = true,
prefix = {"FLAG_"},
- value = {FLAG_ONGOING_SESSION, FLAG_SUGGESTED_ROUTE})
+ value = {FLAG_ONGOING_SESSION, FLAG_SUGGESTED_ROUTE, FLAG_ONGOING_SESSION_MANAGED})
public @interface Flags {}
/**
@@ -269,6 +269,22 @@
public static final int FLAG_ONGOING_SESSION = 1;
/**
+ * Signals that the ongoing session on the corresponding route is managed by the current
+ * user of the app.
+ *
+ * <p>The system can use this flag to provide visual indication that the route is not only
+ * hosting a session, but also that the user has ownership over said session.
+ *
+ * <p>This flag is ignored if {@link #FLAG_ONGOING_SESSION} is not set, or if the
+ * corresponding route is not currently selected.
+ *
+ * <p>This flag does not affect volume adjustment (see {@link VolumeProvider}, and {@link
+ * MediaRoute2Info#getVolumeHandling()}), or any aspect other than the visual representation
+ * of the corresponding item.
+ */
+ public static final int FLAG_ONGOING_SESSION_MANAGED = 1 << 1;
+
+ /**
* The corresponding route is specially likely to be selected by the user.
*
* <p>A UI reflecting this preference may reserve a specific space for suggested routes,
@@ -276,7 +292,7 @@
* number supported by the UI, the routes listed first in {@link
* RouteListingPreference#getItems()} will take priority.
*/
- public static final int FLAG_SUGGESTED_ROUTE = 1 << 1;
+ public static final int FLAG_SUGGESTED_ROUTE = 1 << 2;
/** @hide */
@Retention(RetentionPolicy.SOURCE)
diff --git a/media/java/android/media/tv/AdBuffer.java b/media/java/android/media/tv/AdBuffer.java
index ed44508..230d763 100644
--- a/media/java/android/media/tv/AdBuffer.java
+++ b/media/java/android/media/tv/AdBuffer.java
@@ -24,9 +24,8 @@
/**
* Buffer for advertisement data.
- * @hide
*/
-public class AdBuffer implements Parcelable {
+public final class AdBuffer implements Parcelable {
private final int mId;
@NonNull
private final String mMimeType;
@@ -60,6 +59,8 @@
/**
* Gets corresponding AD request ID.
+ *
+ * @return The ID of the ad request
*/
public int getId() {
return mId;
@@ -67,6 +68,8 @@
/**
* Gets the mime type of the data.
+ *
+ * @return The mime type of the data.
*/
@NonNull
public String getMimeType() {
@@ -74,7 +77,17 @@
}
/**
- * Gets the shared memory which stores the data.
+ * Gets the {@link SharedMemory} which stores the data.
+ *
+ * <p> Information on how the data in this buffer is formatted can be found using
+ * {@link AdRequest#getMetadata()}
+ * <p> This data lives in a {@link SharedMemory} instance because of the
+ * potentially large amount of data needed to store the ad. This optimizes the
+ * data communication between the ad data source and the service responsible for
+ * its display.
+ *
+ * @see SharedMemory#create(String, int)
+ * @return The {@link SharedMemory} that stores the data for this ad buffer.
*/
@NonNull
public SharedMemory getSharedMemory() {
@@ -82,28 +95,38 @@
}
/**
- * Gets the offset of the buffer.
+ * Gets the offset into the shared memory to begin mapping.
+ *
+ * @see SharedMemory#map(int, int, int)
+ * @return The offset of this ad buffer in the shared memory in bytes.
*/
public int getOffset() {
return mOffset;
}
/**
- * Gets the data length.
+ * Gets the data length of this ad buffer.
+ *
+ * @return The data length of this ad buffer in bytes.
*/
public int getLength() {
return mLength;
}
/**
- * Gets the presentation time in microseconds.
+ * Gets the presentation time.
+ *
+ * @return The presentation time in microseconds.
*/
public long getPresentationTimeUs() {
return mPresentationTimeUs;
}
/**
- * Gets the flags.
+ * Gets the buffer flags for this ad buffer.
+ *
+ * @see android.media.MediaCodec
+ * @return The buffer flags for this ad buffer.
*/
@BufferFlag
public int getFlags() {
diff --git a/media/java/android/media/tv/AdRequest.java b/media/java/android/media/tv/AdRequest.java
index 60dfc5e..d8cddfc 100644
--- a/media/java/android/media/tv/AdRequest.java
+++ b/media/java/android/media/tv/AdRequest.java
@@ -79,7 +79,6 @@
mediaFileType, metadata);
}
- /** @hide */
public AdRequest(int id, @RequestType int requestType, @Nullable Uri uri, long startTime,
long stopTime, long echoInterval, @NonNull Bundle metadata) {
this(id, requestType, null, uri, startTime, stopTime, echoInterval, null, metadata);
@@ -153,7 +152,6 @@
*
* @return The URI of the AD media. Can be {@code null} for {@link #REQUEST_TYPE_STOP} or a file
* descriptor is used.
- * @hide
*/
@Nullable
public Uri getUri() {
diff --git a/media/java/android/media/tv/AdResponse.java b/media/java/android/media/tv/AdResponse.java
index a15e8c1..7ec4eb2 100644
--- a/media/java/android/media/tv/AdResponse.java
+++ b/media/java/android/media/tv/AdResponse.java
@@ -43,7 +43,6 @@
public static final int RESPONSE_TYPE_FINISHED = 2;
public static final int RESPONSE_TYPE_STOPPED = 3;
public static final int RESPONSE_TYPE_ERROR = 4;
- /** @hide */
public static final int RESPONSE_TYPE_BUFFERING = 5;
public static final @NonNull Parcelable.Creator<AdResponse> CREATOR =
diff --git a/media/java/android/media/tv/TvInputService.java b/media/java/android/media/tv/TvInputService.java
index 7a4d988d..8166114 100644
--- a/media/java/android/media/tv/TvInputService.java
+++ b/media/java/android/media/tv/TvInputService.java
@@ -1005,9 +1005,10 @@
/**
* Notifies the advertisement buffer is consumed.
- * @hide
+ *
+ * @param buffer the {@link AdBuffer} that was consumed.
*/
- public void notifyAdBufferConsumed(AdBuffer buffer) {
+ public void notifyAdBufferConsumed(@NonNull AdBuffer buffer) {
executeOrPostRunnableOnMainThread(new Runnable() {
@MainThread
@Override
@@ -1320,10 +1321,11 @@
}
/**
- * Called when advertisement buffer is ready.
- * @hide
+ * Called when an advertisement buffer is ready for playback.
+ *
+ * @param buffer The {@link AdBuffer} that became ready for playback.
*/
- public void onAdBuffer(AdBuffer buffer) {
+ public void onAdBuffer(@NonNull AdBuffer buffer) {
}
/**
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
index cdaa3e5..8b85fa1 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
@@ -896,10 +896,10 @@
/**
* Called when an advertisement buffer is consumed.
- * @hide
+ *
+ * @param buffer The {@link AdBuffer} that was consumed.
*/
- public void onAdBufferConsumed(AdBuffer buffer) {
-
+ public void onAdBufferConsumed(@NonNull AdBuffer buffer) {
}
/**
@@ -1919,10 +1919,11 @@
/**
* Notifies when the advertisement buffer is filled and ready to be read.
- * @hide
+ *
+ * @param buffer The {@link AdBuffer} to be received
*/
@CallSuper
- public void notifyAdBuffer(AdBuffer buffer) {
+ public void notifyAdBuffer(@NonNull AdBuffer buffer) {
executeOrPostRunnableOnMainThread(new Runnable() {
@MainThread
@Override
diff --git a/packages/CompanionDeviceManager/res/layout/list_item_permission.xml b/packages/CompanionDeviceManager/res/layout/list_item_permission.xml
index ab2d815..6c463e1 100644
--- a/packages/CompanionDeviceManager/res/layout/list_item_permission.xml
+++ b/packages/CompanionDeviceManager/res/layout/list_item_permission.xml
@@ -22,7 +22,7 @@
android:orientation="horizontal"
android:paddingStart="32dp"
android:paddingEnd="32dp"
- android:paddingBottom="14dp">
+ android:paddingTop="12dp">
<ImageView
android:id="@+id/permission_icon"
diff --git a/packages/CompanionDeviceManager/res/values/strings.xml b/packages/CompanionDeviceManager/res/values/strings.xml
index 7397688..b842761 100644
--- a/packages/CompanionDeviceManager/res/values/strings.xml
+++ b/packages/CompanionDeviceManager/res/values/strings.xml
@@ -20,7 +20,7 @@
<string name="app_label">Companion Device Manager</string>
<!-- Title of the device association confirmation dialog. -->
- <string name="confirmation_title">Allow <strong><xliff:g id="app_name" example="Android Wear">%1$s</xliff:g></strong> to access your <strong><xliff:g id="device_name" example="ASUS ZenWatch 2">%2$s</xliff:g></strong></string>
+ <string name="confirmation_title">Allow <strong><xliff:g id="app_name" example="Android Wear">%1$s</xliff:g></strong> to access <strong><xliff:g id="device_name" example="ASUS ZenWatch 2">%2$s</xliff:g></strong></string>
<!-- ================= DEVICE_PROFILE_WATCH and null profile ================= -->
@@ -31,10 +31,10 @@
<string name="chooser_title">Choose a <xliff:g id="profile_name" example="watch">%1$s</xliff:g> to be managed by <strong><xliff:g id="app_name" example="Android Wear">%2$s</xliff:g></strong></string>
<!-- Description of the privileges the application will get if associated with the companion device of WATCH profile (type) [CHAR LIMIT=NONE] -->
- <string name="summary_watch">The app is needed to manage your <xliff:g id="device_name" example="My Watch">%1$s</xliff:g>. <xliff:g id="app_name" example="Android Wear">%2$s</xliff:g> will be allowed to interact with your notifications and access your Phone, SMS, Contacts, Calendar, Call logs and Nearby devices permissions.</string>
+ <string name="summary_watch">The app is needed to manage your <xliff:g id="device_name" example="My Watch">%1$s</xliff:g>. <xliff:g id="app_name" example="Android Wear">%2$s</xliff:g> will be allowed to sync info, like the name of someone calling, interact with your notifications and access your Phone, SMS, Contacts, Calendar, Call logs and Nearby devices permissions.</string>
<!-- Description of the privileges the application will get if associated with the companion device of WATCH profile for singleDevice(type) [CHAR LIMIT=NONE] -->
- <string name="summary_watch_single_device">The app is needed to manage your <xliff:g id="device_name" example="My Watch">%1$s</xliff:g>. <xliff:g id="app_name" example="Android Wear">%2$s</xliff:g> will be allowed to interact with these permissions:</string>
+ <string name="summary_watch_single_device">The app is needed to manage your <xliff:g id="device_name" example="My Watch">%1$s</xliff:g>. <xliff:g id="app_name" example="Android Wear">%2$s</xliff:g> will be allowed to sync info, like the name of someone calling, and access these permissions:</string>
<!-- TODO(b/256140614) To replace all glasses related strings with final versions -->
<!-- ================= DEVICE_PROFILE_GLASSES ================= -->
@@ -43,10 +43,10 @@
<string name="profile_name_glasses">glasses</string>
<!-- Description of the privileges the application will get if associated with the companion device of GLASSES profile (type) [CHAR LIMIT=NONE] -->
- <string name="summary_glasses">This app is needed to manage your <xliff:g id="device_name" example="My Glasses">%1$s</xliff:g>. <xliff:g id="app_name" example="Glasses">%2$s</xliff:g> will be allowed to interact with your notifications and access your Phone, SMS, Contacts, Microphone and Nearby devices permissions.</string>
+ <string name="summary_glasses">This app is needed to manage <xliff:g id="device_name" example="My Glasses">%1$s</xliff:g>. <xliff:g id="app_name" example="Glasses">%2$s</xliff:g> will be allowed to interact with your notifications and access your Phone, SMS, Contacts, Microphone and Nearby devices permissions.</string>
<!-- Description of the privileges the application will get if associated with the companion device of GLASSES profile for singleDevice(type) [CHAR LIMIT=NONE] -->
- <string name="summary_glasses_single_device">The app is needed to manage your <xliff:g id="device_name" example="My Glasses">%1$s</xliff:g>. <xliff:g id="app_name" example="Glasses">%2$s</xliff:g> will be allowed to interact with these permissions:</string>
+ <string name="summary_glasses_single_device">The app is needed to manage <xliff:g id="device_name" example="My Glasses">%1$s</xliff:g>. <xliff:g id="app_name" example="Glasses">%2$s</xliff:g> will be allowed to interact with these permissions:</string>
<!-- ================= DEVICE_PROFILE_APP_STREAMING ================= -->
@@ -99,7 +99,10 @@
<string name="profile_name_generic">device</string>
<!-- Description of the privileges the application will get if associated with the companion device of unspecified profile (type) [CHAR LIMIT=NONE] -->
- <string name="summary_generic"></string>
+ <string name="summary_generic_single_device">This app will be able to sync info, like the name of someone calling, between your phone and <xliff:g id="device_name" example="My Watch">%1$s</xliff:g>.</string>
+
+ <!-- Description of the privileges the application will get if associated with the companion device of unspecified profile (type) [CHAR LIMIT=NONE] -->
+ <string name="summary_generic">This app will be able to sync info, like the name of someone calling, between your phone and the chosen device.</string>
<!-- ================= Buttons ================= -->
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
index c5ed5c9..918f9c6 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
@@ -546,17 +546,16 @@
}
if (deviceProfile == null) {
- // Summary is not needed for null profile.
- mSummary.setVisibility(View.GONE);
+ summary = getHtmlFromResources(this, SUMMARIES.get(null), deviceName);
mConstraintList.setVisibility(View.GONE);
} else {
+ summary = getHtmlFromResources(this, SUMMARIES.get(deviceProfile),
+ getString(PROFILES_NAME.get(deviceProfile)), appLabel);
mPermissionTypes.addAll(PERMISSION_TYPES.get(deviceProfile));
setupPermissionList();
}
title = getHtmlFromResources(this, TITLES.get(deviceProfile), appLabel, deviceName);
- summary = getHtmlFromResources(this, SUMMARIES.get(deviceProfile),
- getString(PROFILES_NAME.get(deviceProfile)), appLabel);
profileIcon = getIcon(this, PROFILE_ICON.get(deviceProfile));
mTitle.setText(title);
@@ -586,7 +585,6 @@
if (deviceProfile == null) {
summary = getHtmlFromResources(this, summaryResourceId);
- mSummary.setVisibility(View.GONE);
} else {
summary = getHtmlFromResources(this, summaryResourceId, profileName, appLabel);
}
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java
index 6f5f4fe..e3fd354 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java
@@ -88,7 +88,7 @@
final Map<String, Integer> map = new ArrayMap<>();
map.put(DEVICE_PROFILE_WATCH, R.string.summary_watch_single_device);
map.put(DEVICE_PROFILE_GLASSES, R.string.summary_glasses_single_device);
- map.put(null, R.string.summary_generic);
+ map.put(null, R.string.summary_generic_single_device);
SUMMARIES = unmodifiableMap(map);
}
diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml
index d6909719..49ac482 100644
--- a/packages/CredentialManager/res/values/strings.xml
+++ b/packages/CredentialManager/res/values/strings.xml
@@ -13,6 +13,10 @@
<string name="string_more_options">More options</string>
<!-- This is a label for a button that links to additional information about passkeys. [CHAR LIMIT=20] -->
<string name="string_learn_more">Learn more</string>
+ <!-- This is a label for content description for show password icon button. -->
+ <string name="content_description_show_password">Show password</string>
+ <!-- This is a label for content description for hide password icon button. -->
+ <string name="content_description_hide_password">Hide password</string>
<!-- This string introduces passkeys to the users for the first time they use this method. Tip: to avoid gendered language patterns, this header could be translated as if the original string were "More safety with passkeys". [CHAR LIMIT=200] -->
<string name="passkey_creation_intro_title">Safer with passkeys</string>
<!-- This string highlight passkey benefits related with the password. [CHAR LIMIT=200] -->
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
index c66ea5e..6ea1d8d 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
@@ -64,7 +64,7 @@
requestInfo = intent.extras?.getParcelable(
RequestInfo.EXTRA_REQUEST_INFO,
RequestInfo::class.java
- ) ?: testCreatePasskeyRequestInfo()
+ ) ?: testCreatePasswordRequestInfo()
providerEnabledList = when (requestInfo.type) {
RequestInfo.TYPE_CREATE ->
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/ActionButton.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/ActionButton.kt
index d0271ab..984057a 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/ActionButton.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/ActionButton.kt
@@ -16,11 +16,23 @@
package com.android.credentialmanager.common.ui
+import com.android.credentialmanager.R
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Visibility
+import androidx.compose.material.icons.outlined.VisibilityOff
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme
@Composable
fun ActionButton(text: String, onClick: () -> Unit) {
@@ -32,4 +44,27 @@
) {
Text(text = text)
}
+}
+
+@Composable
+fun ToggleVisibilityButton(modifier: Modifier = Modifier, onToggle: (Boolean) -> Unit) {
+ // default state is visibility off
+ val toggleState: MutableState<Boolean> = remember { mutableStateOf(false) }
+
+ IconButton(
+ modifier = modifier,
+ onClick = {
+ toggleState.value = !toggleState.value
+ onToggle(toggleState.value)
+ }
+ ) {
+ Icon(
+ imageVector = if (toggleState.value)
+ Icons.Outlined.Visibility else Icons.Outlined.VisibilityOff,
+ contentDescription = if (toggleState.value)
+ stringResource(R.string.content_description_show_password) else
+ stringResource(R.string.content_description_hide_password),
+ tint = LocalAndroidColorScheme.current.colorAccentPrimaryVariant
+ )
+ }
}
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
index 8fcf025..0b9e578 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
@@ -10,6 +10,8 @@
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
@@ -26,12 +28,17 @@
import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.graphics.drawable.toBitmap
@@ -48,9 +55,9 @@
import com.android.credentialmanager.common.ui.TextSecondary
import com.android.credentialmanager.common.ui.TextOnSurfaceVariant
import com.android.credentialmanager.common.ui.ContainerCard
+import com.android.credentialmanager.common.ui.ToggleVisibilityButton
import com.android.credentialmanager.ui.theme.EntryShape
import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme
-import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -852,12 +859,38 @@
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(top = 16.dp, start = 5.dp),
)
- TextSecondary(
+ Row(modifier = Modifier.fillMaxWidth().padding(top = 4.dp, bottom = 16.dp,
+ start = 5.dp),
+ verticalAlignment = Alignment.CenterVertically) {
+ val visualTransformation = remember { PasswordVisualTransformation() }
// This subtitle would never be null for create password
- text = requestDisplayInfo.subtitle ?: "",
- style = MaterialTheme.typography.bodyMedium,
- modifier = Modifier.padding(bottom = 16.dp, start = 5.dp),
- )
+ val originalPassword by remember {
+ mutableStateOf(requestDisplayInfo.subtitle ?: "")
+ }
+ val displayedPassword = remember {
+ mutableStateOf(
+ visualTransformation.filter(
+ AnnotatedString(originalPassword)
+ ).text.text
+ )
+ }
+ TextSecondary(
+ text = displayedPassword.value,
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.padding(top = 4.dp, bottom = 4.dp),
+ )
+
+ ToggleVisibilityButton(modifier = Modifier.padding(start = 4.dp)
+ .height(24.dp).width(24.dp), onToggle = {
+ if (it) {
+ displayedPassword.value = originalPassword
+ } else {
+ displayedPassword.value = visualTransformation.filter(
+ AnnotatedString(originalPassword)
+ ).text.text
+ }
+ })
+ }
}
CredentialType.UNKNOWN -> {
if (requestDisplayInfo.subtitle != null) {
diff --git a/packages/SettingsLib/ActivityEmbedding/Android.bp b/packages/SettingsLib/ActivityEmbedding/Android.bp
index c35fb3b..4b4cfb7 100644
--- a/packages/SettingsLib/ActivityEmbedding/Android.bp
+++ b/packages/SettingsLib/ActivityEmbedding/Android.bp
@@ -30,5 +30,6 @@
apex_available: [
"//apex_available:platform",
"com.android.permission",
+ "com.android.healthconnect",
],
}
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index 293a590..bf9e428 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -228,6 +228,7 @@
Settings.Secure.BLUETOOTH_LE_BROADCAST_CODE,
Settings.Secure.BLUETOOTH_LE_BROADCAST_APP_SOURCE_NAME,
Settings.Secure.CUSTOM_BUGREPORT_HANDLER_APP,
- Settings.Secure.CUSTOM_BUGREPORT_HANDLER_USER
+ Settings.Secure.CUSTOM_BUGREPORT_HANDLER_USER,
+ Settings.Secure.LOCK_SCREEN_WEATHER_ENABLED
};
}
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index 31eb009..f0bc1df 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -361,5 +361,6 @@
VALIDATORS.put(Secure.BLUETOOTH_LE_BROADCAST_APP_SOURCE_NAME, ANY_STRING_VALIDATOR);
VALIDATORS.put(Secure.CUSTOM_BUGREPORT_HANDLER_APP, ANY_STRING_VALIDATOR);
VALIDATORS.put(Secure.CUSTOM_BUGREPORT_HANDLER_USER, ANY_INTEGER_VALIDATOR);
+ VALIDATORS.put(Secure.LOCK_SCREEN_WEATHER_ENABLED, BOOLEAN_VALIDATOR);
}
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index 3eec565..e55ac1b 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -106,6 +106,12 @@
updateDoubleLineClock();
}
};
+ private final ContentObserver mShowWeatherObserver = new ContentObserver(null) {
+ @Override
+ public void onChange(boolean change) {
+ setWeatherVisibility();
+ }
+ };
private final KeyguardUnlockAnimationController.KeyguardUnlockAnimationListener
mKeyguardUnlockAnimationListener =
@@ -216,7 +222,15 @@
UserHandle.USER_ALL
);
+ mSecureSettings.registerContentObserverForUser(
+ Settings.Secure.LOCK_SCREEN_WEATHER_ENABLED,
+ false, /* notifyForDescendants */
+ mShowWeatherObserver,
+ UserHandle.USER_ALL
+ );
+
updateDoubleLineClock();
+ setWeatherVisibility();
mKeyguardUnlockAnimationController.addKeyguardUnlockAnimationListener(
mKeyguardUnlockAnimationListener);
@@ -449,6 +463,14 @@
}
}
+ private void setWeatherVisibility() {
+ if (mWeatherView != null) {
+ mUiExecutor.execute(
+ () -> mWeatherView.setVisibility(
+ mSmartspaceController.isWeatherEnabled() ? View.VISIBLE : View.GONE));
+ }
+ }
+
/**
* Sets the clipChildren property on relevant views, to allow the smartspace to draw out of
* bounds during the unlock transition.
diff --git a/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt b/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt
index fb0c0a6..5ca36ab 100644
--- a/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt
+++ b/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt
@@ -72,7 +72,7 @@
height = WindowManager.LayoutParams.MATCH_PARENT
layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
format = PixelFormat.TRANSLUCENT
- type = WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY
+ type = WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG
fitInsetsTypes = 0 // Ignore insets from all system bars
title = "Wired Charging Animation"
flags = (WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 57c99d1..5eb7831 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -208,9 +208,7 @@
unreleasedFlag(226, "enable_wallet_contextual_loyalty_cards", teamfood = false)
// TODO(b/242908637): Tracking Bug
- @JvmField
- val WALLPAPER_FULLSCREEN_PREVIEW =
- unreleasedFlag(227, "wallpaper_fullscreen_preview", teamfood = true)
+ @JvmField val WALLPAPER_FULLSCREEN_PREVIEW = releasedFlag(227, "wallpaper_fullscreen_preview")
/** Whether the long-press gesture to open wallpaper picker is enabled. */
// TODO(b/266242192): Tracking Bug
@@ -339,8 +337,7 @@
@JvmField val UMO_TURBULENCE_NOISE = releasedFlag(909, "umo_turbulence_noise")
// TODO(b/263272731): Tracking Bug
- val MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE =
- unreleasedFlag(910, "media_ttt_receiver_success_ripple", teamfood = true)
+ val MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE = releasedFlag(910, "media_ttt_receiver_success_ripple")
// TODO(b/263512203): Tracking Bug
val MEDIA_EXPLICIT_INDICATOR = releasedFlag(911, "media_explicit_indicator")
@@ -507,11 +504,10 @@
// 1300 - screenshots
// TODO(b/254513155): Tracking Bug
@JvmField
- val SCREENSHOT_WORK_PROFILE_POLICY =
- unreleasedFlag(1301, "screenshot_work_profile_policy", teamfood = true)
+ val SCREENSHOT_WORK_PROFILE_POLICY = releasedFlag(1301, "screenshot_work_profile_policy")
// TODO(b/264916608): Tracking Bug
- @JvmField val SCREENSHOT_METADATA = unreleasedFlag(1302, "screenshot_metadata")
+ @JvmField val SCREENSHOT_METADATA = unreleasedFlag(1302, "screenshot_metadata", teamfood = true)
// TODO(b/266955521): Tracking bug
@JvmField val SCREENSHOT_DETECTION = unreleasedFlag(1303, "screenshot_detection")
@@ -529,16 +525,19 @@
// 1500 - chooser aka sharesheet
// TODO(b/254512507): Tracking Bug
- val CHOOSER_UNBUNDLED = unreleasedFlag(1500, "chooser_unbundled", teamfood = true)
+ val CHOOSER_UNBUNDLED = releasedFlag(1500, "chooser_unbundled")
// TODO(b/266983432) Tracking Bug
- val SHARESHEET_CUSTOM_ACTIONS = unreleasedFlag(1501, "sharesheet_custom_actions")
+ val SHARESHEET_CUSTOM_ACTIONS =
+ unreleasedFlag(1501, "sharesheet_custom_actions", teamfood = true)
// TODO(b/266982749) Tracking Bug
- val SHARESHEET_RESELECTION_ACTION = unreleasedFlag(1502, "sharesheet_reselection_action")
+ val SHARESHEET_RESELECTION_ACTION =
+ unreleasedFlag(1502, "sharesheet_reselection_action", teamfood = true)
// TODO(b/266983474) Tracking Bug
- val SHARESHEET_IMAGE_AND_TEXT_PREVIEW = unreleasedFlag(1503, "sharesheet_image_text_preview")
+ val SHARESHEET_IMAGE_AND_TEXT_PREVIEW =
+ unreleasedFlag(1503, "sharesheet_image_text_preview", teamfood = true)
// TODO(b/267355521) Tracking Bug
val SHARESHEET_SCROLLABLE_IMAGE_PREVIEW =
@@ -597,12 +596,11 @@
// 2500 - output switcher
// TODO(b/261538825): Tracking Bug
@JvmField
- val OUTPUT_SWITCHER_ADVANCED_LAYOUT = unreleasedFlag(2500, "output_switcher_advanced_layout")
+ val OUTPUT_SWITCHER_ADVANCED_LAYOUT = releasedFlag(2500, "output_switcher_advanced_layout")
@JvmField
- val OUTPUT_SWITCHER_ROUTES_PROCESSING =
- unreleasedFlag(2501, "output_switcher_routes_processing")
+ val OUTPUT_SWITCHER_ROUTES_PROCESSING = releasedFlag(2501, "output_switcher_routes_processing")
@JvmField
- val OUTPUT_SWITCHER_DEVICE_STATUS = unreleasedFlag(2502, "output_switcher_device_status")
+ val OUTPUT_SWITCHER_DEVICE_STATUS = releasedFlag(2502, "output_switcher_device_status")
// TODO(b/20911786): Tracking Bug
@JvmField
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
index 3319f9d..ab009f4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
@@ -381,82 +381,87 @@
return when (event?.actionMasked) {
MotionEvent.ACTION_DOWN ->
if (viewModel.configKey != null) {
- longPressAnimator =
- view
- .animate()
- .scaleX(PRESSED_SCALE)
- .scaleY(PRESSED_SCALE)
- .setDuration(longPressDurationMs)
- .withEndAction {
- view.setOnClickListener {
- vibratorHelper?.vibrate(
- if (viewModel.isActivated) {
- Vibrations.Activated
- } else {
- Vibrations.Deactivated
- }
- )
- viewModel.onClicked(
- KeyguardQuickAffordanceViewModel.OnClickedParameters(
- configKey = viewModel.configKey,
- expandable = Expandable.fromView(view),
- )
- )
+ if (isUsingAccurateTool(event)) {
+ // For accurate tool types (stylus, mouse, etc.), we don't require a
+ // long-press.
+ } else {
+ // When not using a stylus, we require a long-press to activate the
+ // quick affordance, mostly to do "falsing" (e.g. protect from false
+ // clicks in the pocket/bag).
+ longPressAnimator =
+ view
+ .animate()
+ .scaleX(PRESSED_SCALE)
+ .scaleY(PRESSED_SCALE)
+ .setDuration(longPressDurationMs)
+ .withEndAction {
+ dispatchClick(viewModel.configKey)
+ cancel()
}
- view.performClick()
- view.setOnClickListener(null)
- cancel()
- }
+ }
true
} else {
false
}
MotionEvent.ACTION_MOVE -> {
- if (event.historySize > 0) {
- val distance =
- sqrt(
- (event.y - event.getHistoricalY(0)).pow(2) +
- (event.x - event.getHistoricalX(0)).pow(2)
- )
- if (distance > ViewConfiguration.getTouchSlop()) {
+ if (!isUsingAccurateTool(event)) {
+ // Moving too far while performing a long-press gesture cancels that
+ // gesture.
+ val distanceMoved = distanceMoved(event)
+ if (distanceMoved > ViewConfiguration.getTouchSlop()) {
cancel()
}
}
true
}
MotionEvent.ACTION_UP -> {
- cancel(
- onAnimationEnd =
- if (event.eventTime - event.downTime < longPressDurationMs) {
- Runnable {
- messageDisplayer.invoke(
- R.string.keyguard_affordance_press_too_short
- )
- val amplitude =
- view.context.resources
- .getDimensionPixelSize(
- R.dimen.keyguard_affordance_shake_amplitude
- )
- .toFloat()
- val shakeAnimator =
- ObjectAnimator.ofFloat(
- view,
- "translationX",
- -amplitude / 2,
- amplitude / 2,
+ if (isUsingAccurateTool(event)) {
+ // When using an accurate tool type (stylus, mouse, etc.), we don't require
+ // a long-press gesture to activate the quick affordance. Therefore, lifting
+ // the pointer performs a click.
+ if (
+ viewModel.configKey != null &&
+ distanceMoved(event) <= ViewConfiguration.getTouchSlop()
+ ) {
+ dispatchClick(viewModel.configKey)
+ }
+ } else {
+ // When not using a stylus, lifting the finger/pointer will actually cancel
+ // the long-press gesture. Calling cancel after the quick affordance was
+ // already long-press activated is a no-op, so it's safe to call from here.
+ cancel(
+ onAnimationEnd =
+ if (event.eventTime - event.downTime < longPressDurationMs) {
+ Runnable {
+ messageDisplayer.invoke(
+ R.string.keyguard_affordance_press_too_short
)
- shakeAnimator.duration =
- ShakeAnimationDuration.inWholeMilliseconds
- shakeAnimator.interpolator =
- CycleInterpolator(ShakeAnimationCycles)
- shakeAnimator.start()
+ val amplitude =
+ view.context.resources
+ .getDimensionPixelSize(
+ R.dimen.keyguard_affordance_shake_amplitude
+ )
+ .toFloat()
+ val shakeAnimator =
+ ObjectAnimator.ofFloat(
+ view,
+ "translationX",
+ -amplitude / 2,
+ amplitude / 2,
+ )
+ shakeAnimator.duration =
+ ShakeAnimationDuration.inWholeMilliseconds
+ shakeAnimator.interpolator =
+ CycleInterpolator(ShakeAnimationCycles)
+ shakeAnimator.start()
- vibratorHelper?.vibrate(Vibrations.Shake)
+ vibratorHelper?.vibrate(Vibrations.Shake)
+ }
+ } else {
+ null
}
- } else {
- null
- }
- )
+ )
+ }
true
}
MotionEvent.ACTION_CANCEL -> {
@@ -467,6 +472,28 @@
}
}
+ private fun dispatchClick(
+ configKey: String,
+ ) {
+ view.setOnClickListener {
+ vibratorHelper?.vibrate(
+ if (viewModel.isActivated) {
+ Vibrations.Activated
+ } else {
+ Vibrations.Deactivated
+ }
+ )
+ viewModel.onClicked(
+ KeyguardQuickAffordanceViewModel.OnClickedParameters(
+ configKey = configKey,
+ expandable = Expandable.fromView(view),
+ )
+ )
+ }
+ view.performClick()
+ view.setOnClickListener(null)
+ }
+
private fun cancel(onAnimationEnd: Runnable? = null) {
longPressAnimator?.cancel()
longPressAnimator = null
@@ -475,6 +502,40 @@
companion object {
private const val PRESSED_SCALE = 1.5f
+
+ /**
+ * Returns `true` if the tool type at the given pointer index is an accurate tool (like
+ * stylus or mouse), which means we can trust it to not be a false click; `false`
+ * otherwise.
+ */
+ private fun isUsingAccurateTool(
+ event: MotionEvent,
+ pointerIndex: Int = 0,
+ ): Boolean {
+ return when (event.getToolType(pointerIndex)) {
+ MotionEvent.TOOL_TYPE_STYLUS -> true
+ MotionEvent.TOOL_TYPE_MOUSE -> true
+ else -> false
+ }
+ }
+
+ /**
+ * Returns the amount of distance the pointer moved since the historical record at the
+ * [since] index.
+ */
+ private fun distanceMoved(
+ event: MotionEvent,
+ since: Int = 0,
+ ): Float {
+ return if (event.historySize > 0) {
+ sqrt(
+ (event.y - event.getHistoricalY(since)).pow(2) +
+ (event.x - event.getHistoricalX(since)).pow(2)
+ )
+ } else {
+ 0f
+ }
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/Utilities.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/Utilities.java
index 335172e..30d2d7a 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/Utilities.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/Utilities.java
@@ -16,7 +16,7 @@
package com.android.systemui.navigationbar.gestural;
-import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
+import static android.view.MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE;
import android.content.Context;
import android.view.MotionEvent;
@@ -24,14 +24,12 @@
public final class Utilities {
- private static final int TRACKPAD_GESTURE_SCALE = 60;
+ private static final int TRACKPAD_GESTURE_SCALE = 200;
public static boolean isTrackpadMotionEvent(boolean isTrackpadGestureBackEnabled,
MotionEvent event) {
- // TODO: ideally should use event.getClassification(), but currently only the move
- // events get assigned the correct classification.
return isTrackpadGestureBackEnabled
- && (event.getSource() & SOURCE_TOUCHSCREEN) != SOURCE_TOUCHSCREEN;
+ && event.getClassification() == CLASSIFICATION_MULTI_FINGER_SWIPE;
}
public static int getTrackpadScale(Context context) {
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
index 6bfe1a0..be615d6 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
@@ -20,9 +20,12 @@
import android.content.ActivityNotFoundException
import android.content.ComponentName
import android.content.Context
+import android.content.Intent
import android.content.pm.PackageManager
import android.os.UserManager
import android.util.Log
+import com.android.internal.logging.UiEvent
+import com.android.internal.logging.UiEventLogger
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
import com.android.systemui.util.kotlin.getOrNull
@@ -42,11 +45,12 @@
@Inject
constructor(
private val context: Context,
- private val intentResolver: NoteTaskIntentResolver,
+ private val resolver: NoteTaskInfoResolver,
private val optionalBubbles: Optional<Bubbles>,
private val optionalKeyguardManager: Optional<KeyguardManager>,
private val optionalUserManager: Optional<UserManager>,
@NoteTaskEnabledKey private val isEnabled: Boolean,
+ private val uiEventLogger: UiEventLogger,
) {
/**
@@ -64,7 +68,9 @@
*
* That will let users open other apps in full screen, and take contextual notes.
*/
- fun showNoteTask(isInMultiWindowMode: Boolean = false) {
+ @JvmOverloads
+ fun showNoteTask(isInMultiWindowMode: Boolean = false, uiEvent: ShowNoteTaskUiEvent? = null) {
+
if (!isEnabled) return
val bubbles = optionalBubbles.getOrNull() ?: return
@@ -74,9 +80,12 @@
// TODO(b/249954038): We should handle direct boot (isUserUnlocked). For now, we do nothing.
if (!userManager.isUserUnlocked) return
- val intent = intentResolver.resolveIntent() ?: return
+ val noteTaskInfo = resolver.resolveInfo() ?: return
+
+ uiEvent?.let { uiEventLogger.log(it, noteTaskInfo.uid, noteTaskInfo.packageName) }
// TODO(b/266686199): We should handle when app not available. For now, we log.
+ val intent = noteTaskInfo.toCreateNoteIntent()
try {
if (isInMultiWindowMode || keyguardManager.isKeyguardLocked) {
context.startActivity(intent)
@@ -84,9 +93,7 @@
bubbles.showOrHideAppBubble(intent)
}
} catch (e: ActivityNotFoundException) {
- val message =
- "Activity not found for action: ${NoteTaskIntentResolver.ACTION_CREATE_NOTE}."
- Log.e(TAG, message, e)
+ Log.e(TAG, "Activity not found for action: $ACTION_CREATE_NOTE.", e)
}
}
@@ -114,10 +121,47 @@
)
}
+ /** IDs of UI events accepted by [showNoteTask]. */
+ enum class ShowNoteTaskUiEvent(private val _id: Int) : UiEventLogger.UiEventEnum {
+ @UiEvent(doc = "User opened a note by tapping on the lockscreen shortcut.")
+ NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE(1294),
+
+ /* ktlint-disable max-line-length */
+ @UiEvent(
+ doc =
+ "User opened a note by pressing the stylus tail button while the screen was unlocked."
+ )
+ NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON(1295),
+ @UiEvent(
+ doc =
+ "User opened a note by pressing the stylus tail button while the screen was locked."
+ )
+ NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON_LOCKED(1296),
+ @UiEvent(doc = "User opened a note by tapping on an app shortcut.")
+ NOTE_OPENED_VIA_SHORTCUT(1297);
+
+ override fun getId() = _id
+ }
+
companion object {
private val TAG = NoteTaskController::class.simpleName.orEmpty()
+ private fun NoteTaskInfoResolver.NoteTaskInfo.toCreateNoteIntent(): Intent {
+ return Intent(ACTION_CREATE_NOTE)
+ .setPackage(packageName)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ // EXTRA_USE_STYLUS_MODE does not mean a stylus is in-use, but a stylus entrypoint
+ // was used to start it.
+ .putExtra(INTENT_EXTRA_USE_STYLUS_MODE, true)
+ }
+
// TODO(b/254604589): Use final KeyEvent.KEYCODE_* instead.
const val NOTE_TASK_KEY_EVENT = 311
+
+ // TODO(b/265912743): Use Intent.ACTION_CREATE_NOTE instead.
+ const val ACTION_CREATE_NOTE = "android.intent.action.CREATE_NOTE"
+
+ // TODO(b/265912743): Use Intent.INTENT_EXTRA_USE_STYLUS_MODE instead.
+ const val INTENT_EXTRA_USE_STYLUS_MODE = "android.intent.extra.USE_STYLUS_MODE"
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfoResolver.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfoResolver.kt
new file mode 100644
index 0000000..bd822d4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfoResolver.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2023 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.systemui.notetask
+
+import android.app.role.RoleManager
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.UserHandle
+import android.util.Log
+import javax.inject.Inject
+
+internal class NoteTaskInfoResolver
+@Inject
+constructor(
+ private val context: Context,
+ private val roleManager: RoleManager,
+ private val packageManager: PackageManager,
+) {
+ fun resolveInfo(): NoteTaskInfo? {
+ // TODO(b/267634412): Select UserHandle depending on where the user initiated note-taking.
+ val user = context.user
+ val packageName = roleManager.getRoleHoldersAsUser(ROLE_NOTES, user).firstOrNull()
+
+ if (packageName.isNullOrEmpty()) return null
+
+ return NoteTaskInfo(packageName, packageManager.getUidOf(packageName, user))
+ }
+
+ /** Package name and kernel user-ID of a note-taking app. */
+ data class NoteTaskInfo(val packageName: String, val uid: Int)
+
+ companion object {
+ private val TAG = NoteTaskInfoResolver::class.simpleName.orEmpty()
+
+ private val EMPTY_APPLICATION_INFO_FLAGS = PackageManager.ApplicationInfoFlags.of(0)!!
+
+ /**
+ * Returns the kernel user-ID of [packageName] for a [user]. Returns zero if the app cannot
+ * be found.
+ */
+ private fun PackageManager.getUidOf(packageName: String, user: UserHandle): Int {
+ val applicationInfo =
+ try {
+ getApplicationInfoAsUser(packageName, EMPTY_APPLICATION_INFO_FLAGS, user)
+ } catch (e: PackageManager.NameNotFoundException) {
+ Log.e(TAG, "Couldn't find notes app UID", e)
+ return 0
+ }
+ return applicationInfo.uid
+ }
+
+ // TODO(b/265912743): Use RoleManager.NOTES_ROLE instead.
+ const val ROLE_NOTES = "android.app.role.NOTES"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
index d5f4a5a..d40bf2b 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
@@ -16,8 +16,10 @@
package com.android.systemui.notetask
+import android.app.KeyguardManager
import androidx.annotation.VisibleForTesting
import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.util.kotlin.getOrNull
import com.android.wm.shell.bubbles.Bubbles
import java.util.Optional
import javax.inject.Inject
@@ -30,6 +32,7 @@
private val noteTaskController: NoteTaskController,
private val commandQueue: CommandQueue,
@NoteTaskEnabledKey private val isEnabled: Boolean,
+ private val optionalKeyguardManager: Optional<KeyguardManager>,
) {
@VisibleForTesting
@@ -37,11 +40,21 @@
object : CommandQueue.Callbacks {
override fun handleSystemKey(keyCode: Int) {
if (keyCode == NoteTaskController.NOTE_TASK_KEY_EVENT) {
- noteTaskController.showNoteTask()
+ showNoteTask()
}
}
}
+ private fun showNoteTask() {
+ val uiEvent =
+ if (optionalKeyguardManager.isKeyguardLocked) {
+ NoteTaskController.ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON_LOCKED
+ } else {
+ NoteTaskController.ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON
+ }
+ noteTaskController.showNoteTask(uiEvent = uiEvent)
+ }
+
fun initialize() {
if (isEnabled && optionalBubbles.isPresent) {
commandQueue.addCallback(callbacks)
@@ -49,3 +62,7 @@
noteTaskController.setNoteTaskShortcutEnabled(isEnabled)
}
}
+
+private val Optional<KeyguardManager>.isKeyguardLocked: Boolean
+ // If there's no KeyguardManager, assume that the keyguard is not locked.
+ get() = getOrNull()?.isKeyguardLocked ?: false
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt
deleted file mode 100644
index 11dc1d7..0000000
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2022 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.systemui.notetask
-
-import android.app.role.RoleManager
-import android.content.Context
-import android.content.Intent
-import javax.inject.Inject
-
-internal class NoteTaskIntentResolver
-@Inject
-constructor(
- private val context: Context,
- private val roleManager: RoleManager,
-) {
-
- fun resolveIntent(): Intent? {
- val packageName = roleManager.getRoleHoldersAsUser(ROLE_NOTES, context.user).firstOrNull()
-
- if (packageName.isNullOrEmpty()) return null
-
- return Intent(ACTION_CREATE_NOTE)
- .setPackage(packageName)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- // EXTRA_USE_STYLUS_MODE does not mean a stylus is in-use, but a stylus entrypoint was
- // used to start it.
- .putExtra(INTENT_EXTRA_USE_STYLUS_MODE, true)
- }
-
- companion object {
- // TODO(b/265912743): Use Intent.ACTION_CREATE_NOTE instead.
- const val ACTION_CREATE_NOTE = "android.intent.action.CREATE_NOTE"
-
- // TODO(b/265912743): Use RoleManager.NOTES_ROLE instead.
- const val ROLE_NOTES = "android.app.role.NOTES"
-
- // TODO(b/265912743): Use Intent.INTENT_EXTRA_USE_STYLUS_MODE instead.
- const val INTENT_EXTRA_USE_STYLUS_MODE = "android.intent.extra.USE_STYLUS_MODE"
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
index ec6a16a..b8800a2 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
@@ -51,7 +51,7 @@
featureFlags: FeatureFlags,
roleManager: RoleManager,
): Boolean {
- val isRoleAvailable = roleManager.isRoleAvailable(NoteTaskIntentResolver.ROLE_NOTES)
+ val isRoleAvailable = roleManager.isRoleAvailable(NoteTaskInfoResolver.ROLE_NOTES)
val isFeatureEnabled = featureFlags.isEnabled(Flags.NOTE_TASKS)
return isRoleAvailable && isFeatureEnabled
}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt
index cfbaa48..43869cc 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt
@@ -27,6 +27,7 @@
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.OnTriggeredResult
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.PickerScreenState
import com.android.systemui.notetask.NoteTaskController
+import com.android.systemui.notetask.NoteTaskController.ShowNoteTaskUiEvent
import com.android.systemui.notetask.NoteTaskEnabledKey
import javax.inject.Inject
import kotlinx.coroutines.flow.flowOf
@@ -64,7 +65,9 @@
}
override fun onTriggered(expandable: Expandable?): OnTriggeredResult {
- noteTaskController.showNoteTask()
+ noteTaskController.showNoteTask(
+ uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE
+ )
return OnTriggeredResult.Handled
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
index f203e7a..3ac5bfa 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
@@ -21,7 +21,7 @@
import android.os.Bundle
import androidx.activity.ComponentActivity
import com.android.systemui.notetask.NoteTaskController
-import com.android.systemui.notetask.NoteTaskIntentResolver
+import com.android.systemui.notetask.NoteTaskController.ShowNoteTaskUiEvent
import javax.inject.Inject
/** Activity responsible for launching the note experience, and finish. */
@@ -34,7 +34,10 @@
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- noteTaskController.showNoteTask(isInMultiWindowMode)
+ noteTaskController.showNoteTask(
+ isInMultiWindowMode = isInMultiWindowMode,
+ uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_SHORTCUT,
+ )
finish()
}
@@ -46,7 +49,7 @@
return Intent(context, LaunchNoteTaskActivity::class.java).apply {
// Intent's action must be set in shortcuts, or an exception will be thrown.
// TODO(b/254606432): Use Intent.ACTION_CREATE_NOTE instead.
- action = NoteTaskIntentResolver.ACTION_CREATE_NOTE
+ action = NoteTaskController.ACTION_CREATE_NOTE
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index c573080..f53f824 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -685,6 +685,7 @@
private boolean mInstantExpanding;
private boolean mAnimateAfterExpanding;
private boolean mIsFlinging;
+ private boolean mLastFlingWasExpanding;
private String mViewName;
private float mInitialExpandY;
private float mInitialExpandX;
@@ -2142,6 +2143,7 @@
@VisibleForTesting
void flingToHeight(float vel, boolean expand, float target,
float collapseSpeedUpFactor, boolean expandBecauseOfFalsing) {
+ mLastFlingWasExpanding = expand;
mHeadsUpTouchHelper.notifyFling(!expand);
mKeyguardStateController.notifyPanelFlingStart(!expand /* flingingToDismiss */);
setClosingWithAlphaFadeout(!expand && !isOnKeyguard() && getFadeoutAlpha() == 1.0f);
@@ -2531,7 +2533,7 @@
}
// defer touches on QQS to shade while shade is collapsing. Added margin for error
// as sometimes the qsExpansionFraction can be a tiny value instead of 0 when in QQS.
- if (!mSplitShadeEnabled
+ if (!mSplitShadeEnabled && !mLastFlingWasExpanding
&& computeQsExpansionFraction() <= 0.01 && getExpandedFraction() < 1.0) {
mShadeLog.logMotionEvent(event,
"handleQsTouch: shade touched while collapsing, QS tracking disabled");
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
index fe76c7d..81c7197 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
@@ -31,6 +31,7 @@
import android.os.UserHandle
import android.provider.Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS
import android.provider.Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS
+import android.provider.Settings.Secure.LOCK_SCREEN_WEATHER_ENABLED
import android.util.Log
import android.view.ContextThemeWrapper
import android.view.View
@@ -245,6 +246,17 @@
datePlugin != null && weatherPlugin != null
}
+ fun isWeatherEnabled(): Boolean {
+ execution.assertIsMainThread()
+ val defaultValue = context.getResources().getBoolean(
+ com.android.internal.R.bool.config_lockscreenWeatherEnabledByDefault)
+ val showWeather = secureSettings.getIntForUser(
+ LOCK_SCREEN_WEATHER_ENABLED,
+ if (defaultValue) 1 else 0,
+ userTracker.userId) == 1
+ return showWeather
+ }
+
private fun updateBypassEnabled() {
val bypassEnabled = bypassController.bypassEnabled
smartspaceViews.forEach { it.setKeyguardBypassEnabled(bypassEnabled) }
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
index e5ab473..5cf01af 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
@@ -76,6 +76,9 @@
/** Whether user switching is currently in progress. */
val userSwitchingInProgress: Flow<Boolean>
+ /** User ID of the main user. */
+ val mainUserId: Int
+
/** User ID of the last non-guest selected user. */
val lastSelectedNonGuestUserId: Int
@@ -130,7 +133,9 @@
private val _selectedUserInfo = MutableStateFlow<UserInfo?>(null)
override val selectedUserInfo: Flow<UserInfo> = _selectedUserInfo.filterNotNull()
- override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM
+ override var mainUserId: Int = UserHandle.USER_NULL
+ private set
+ override var lastSelectedNonGuestUserId: Int = UserHandle.USER_NULL
private set
override val isGuestUserAutoCreated: Boolean =
@@ -172,6 +177,11 @@
// The guest user is always last, regardless of creation time.
.sortedBy { it.isGuest }
}
+
+ if (mainUserId == UserHandle.USER_NULL) {
+ val mainUser = withContext(backgroundDispatcher) { manager.mainUser }
+ mainUser?.let { mainUserId = it.identifier }
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt
index a374885..0a07439 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt
@@ -139,11 +139,11 @@
}
applicationScope.launch {
- var newUserId = UserHandle.USER_SYSTEM
+ var newUserId = repository.mainUserId
if (targetUserId == UserHandle.USER_NULL) {
// When a target user is not specified switch to last non guest user:
val lastSelectedNonGuestUserHandle = repository.lastSelectedNonGuestUserId
- if (lastSelectedNonGuestUserHandle != UserHandle.USER_SYSTEM) {
+ if (lastSelectedNonGuestUserHandle != repository.mainUserId) {
val info =
withContext(backgroundDispatcher) {
manager.getUserInfo(lastSelectedNonGuestUserHandle)
@@ -215,8 +215,11 @@
// Create a new guest in the foreground, and then immediately switch to it
val newGuestId = create(showDialog, dismissDialog)
if (newGuestId == UserHandle.USER_NULL) {
- Log.e(TAG, "Could not create new guest, switching back to system user")
- switchUser(UserHandle.USER_SYSTEM)
+ Log.e(TAG, "Could not create new guest, switching back to main user")
+ val mainUser = withContext(backgroundDispatcher) { manager.mainUser?.identifier }
+
+ mainUser?.let { switchUser(it) }
+
withContext(backgroundDispatcher) {
manager.removeUserWhenPossible(
UserHandle.of(currentUser.id),
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
index 36b3f89..ccc4e4a 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
@@ -300,8 +300,9 @@
ArgumentCaptor<ContentObserver> observerCaptor =
ArgumentCaptor.forClass(ContentObserver.class);
mController.init();
- verify(mSecureSettings).registerContentObserverForUser(any(String.class),
- anyBoolean(), observerCaptor.capture(), eq(UserHandle.USER_ALL));
+ verify(mSecureSettings).registerContentObserverForUser(
+ eq(Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK),
+ anyBoolean(), observerCaptor.capture(), eq(UserHandle.USER_ALL));
ContentObserver observer = observerCaptor.getValue();
mExecutor.runAllReady();
@@ -347,6 +348,22 @@
assertEquals(0, mController.getClockBottom(10));
}
+ @Test
+ public void testChangeLockscreenWeatherEnabledSetsWeatherViewVisible() {
+ when(mSmartspaceController.isWeatherEnabled()).thenReturn(true);
+ ArgumentCaptor<ContentObserver> observerCaptor =
+ ArgumentCaptor.forClass(ContentObserver.class);
+ mController.init();
+ verify(mSecureSettings).registerContentObserverForUser(
+ eq(Settings.Secure.LOCK_SCREEN_WEATHER_ENABLED), anyBoolean(),
+ observerCaptor.capture(), eq(UserHandle.USER_ALL));
+ ContentObserver observer = observerCaptor.getValue();
+ mExecutor.runAllReady();
+ // When a settings change has occurred, check that view is visible.
+ observer.onChange(true);
+ mExecutor.runAllReady();
+ assertEquals(View.VISIBLE, mFakeWeatherView.getVisibility());
+ }
private void verifyAttachment(VerificationMode times) {
verify(mClockRegistry, times).registerClockChangeListener(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt
index 32edf8f..babbe45 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt
@@ -11,6 +11,7 @@
import com.android.systemui.flags.Flag
import com.android.systemui.flags.FlagListenable
import com.android.systemui.flags.Flags
+import com.android.systemui.flags.ReleasedFlag
import com.android.systemui.flags.UnreleasedFlag
import com.android.systemui.settings.UserTracker
import com.android.systemui.util.mockito.any
@@ -102,7 +103,7 @@
@Test
fun initialize_enablesUnbundledChooser_whenFlagEnabled() {
// Arrange
- whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(true)
+ setFlagMock(true)
// Act
chooserSelector.start()
@@ -118,7 +119,7 @@
@Test
fun initialize_disablesUnbundledChooser_whenFlagDisabled() {
// Arrange
- whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
+ setFlagMock(false)
// Act
chooserSelector.start()
@@ -134,7 +135,7 @@
@Test
fun enablesUnbundledChooser_whenFlagBecomesEnabled() {
// Arrange
- whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
+ setFlagMock(false)
chooserSelector.start()
verify(mockFeatureFlags).addListener(
eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED),
@@ -147,7 +148,7 @@
)
// Act
- whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(true)
+ setFlagMock(true)
flagListener.value.onFlagChanged(TestFlagEvent(Flags.CHOOSER_UNBUNDLED.name))
// Assert
@@ -161,7 +162,7 @@
@Test
fun disablesUnbundledChooser_whenFlagBecomesDisabled() {
// Arrange
- whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(true)
+ setFlagMock(true)
chooserSelector.start()
verify(mockFeatureFlags).addListener(
eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED),
@@ -174,7 +175,7 @@
)
// Act
- whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
+ setFlagMock(false)
flagListener.value.onFlagChanged(TestFlagEvent(Flags.CHOOSER_UNBUNDLED.name))
// Assert
@@ -188,7 +189,7 @@
@Test
fun doesNothing_whenAnotherFlagChanges() {
// Arrange
- whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
+ setFlagMock(false)
chooserSelector.start()
verify(mockFeatureFlags).addListener(
eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED),
@@ -197,13 +198,17 @@
clearInvocations(mockPackageManager)
// Act
- whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
flagListener.value.onFlagChanged(TestFlagEvent("other flag"))
// Assert
verifyZeroInteractions(mockPackageManager)
}
+ private fun setFlagMock(enabled: Boolean) {
+ whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(enabled)
+ whenever(mockFeatureFlags.isEnabled(any<ReleasedFlag>())).thenReturn(enabled)
+ }
+
private class TestFlagEvent(override val flagName: String) : FlagListenable.FlagEvent {
override fun requestNoRestart() {}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/MotionEventsHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/MotionEventsHandlerTest.java
index 509d5f0..70ba306 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/MotionEventsHandlerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/MotionEventsHandlerTest.java
@@ -16,10 +16,10 @@
package com.android.systemui.navigationbar.gestural;
-import static android.view.InputDevice.SOURCE_TOUCHPAD;
-import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
+import static android.view.InputDevice.SOURCE_MOUSE;
import static android.view.MotionEvent.AXIS_GESTURE_X_OFFSET;
import static android.view.MotionEvent.AXIS_GESTURE_Y_OFFSET;
+import static android.view.MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE;
import static com.google.common.truth.Truth.assertThat;
@@ -57,14 +57,9 @@
@Test
public void onTouchEvent_touchScreen_hasCorrectDisplacements() {
MotionEvent down = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 100, 100, 0);
- // TODO: change to use classification after gesture library is ported.
- down.setSource(SOURCE_TOUCHSCREEN);
MotionEvent move1 = MotionEvent.obtain(0, 1, MotionEvent.ACTION_MOVE, 150, 125, 0);
- move1.setSource(SOURCE_TOUCHSCREEN);
MotionEvent move2 = MotionEvent.obtain(0, 2, MotionEvent.ACTION_MOVE, 200, 150, 0);
- move2.setSource(SOURCE_TOUCHSCREEN);
MotionEvent up = MotionEvent.obtain(0, 3, MotionEvent.ACTION_UP, 250, 175, 0);
- up.setSource(SOURCE_TOUCHSCREEN);
mMotionEventsHandler.onMotionEvent(down);
mMotionEventsHandler.onMotionEvent(move1);
@@ -90,8 +85,8 @@
downPointerProperties[0].id = 1;
downPointerProperties[0].toolType = MotionEvent.TOOL_TYPE_FINGER;
MotionEvent down = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 1,
- downPointerProperties, downPointerCoords, 0, 0, 1.0f, 1.0f, 0, 0,
- SOURCE_TOUCHPAD, 0);
+ downPointerProperties, downPointerCoords, 0, 0, 1.0f, 1.0f, 0, 0, SOURCE_MOUSE,
+ 0, 0, CLASSIFICATION_MULTI_FINGER_SWIPE);
MotionEvent.PointerCoords[] movePointerCoords1 = new MotionEvent.PointerCoords[1];
movePointerCoords1[0] = new MotionEvent.PointerCoords();
@@ -103,8 +98,8 @@
movePointerProperties1[0].id = 1;
movePointerProperties1[0].toolType = MotionEvent.TOOL_TYPE_FINGER;
MotionEvent move1 = MotionEvent.obtain(0, 1, MotionEvent.ACTION_MOVE, 1,
- movePointerProperties1, movePointerCoords1, 0, 0, 1.0f, 1.0f, 0, 0, SOURCE_TOUCHPAD,
- 0);
+ movePointerProperties1, movePointerCoords1, 0, 0, 1.0f, 1.0f, 0, 0, SOURCE_MOUSE,
+ 0, 0, CLASSIFICATION_MULTI_FINGER_SWIPE);
MotionEvent.PointerCoords[] movePointerCoords2 = new MotionEvent.PointerCoords[1];
movePointerCoords2[0] = new MotionEvent.PointerCoords();
@@ -116,8 +111,8 @@
movePointerProperties2[0].id = 1;
movePointerProperties2[0].toolType = MotionEvent.TOOL_TYPE_FINGER;
MotionEvent move2 = MotionEvent.obtain(0, 2, MotionEvent.ACTION_MOVE, 1,
- movePointerProperties2, movePointerCoords2, 0, 0, 1.0f, 1.0f, 0, 0, SOURCE_TOUCHPAD,
- 0);
+ movePointerProperties2, movePointerCoords2, 0, 0, 1.0f, 1.0f, 0, 0, SOURCE_MOUSE,
+ 0, 0, CLASSIFICATION_MULTI_FINGER_SWIPE);
MotionEvent.PointerCoords[] upPointerCoords = new MotionEvent.PointerCoords[1];
upPointerCoords[0] = new MotionEvent.PointerCoords();
@@ -129,7 +124,8 @@
upPointerProperties2[0].id = 1;
upPointerProperties2[0].toolType = MotionEvent.TOOL_TYPE_FINGER;
MotionEvent up = MotionEvent.obtain(0, 2, MotionEvent.ACTION_UP, 1,
- upPointerProperties2, upPointerCoords, 0, 0, 1.0f, 1.0f, 0, 0, SOURCE_TOUCHPAD, 0);
+ upPointerProperties2, upPointerCoords, 0, 0, 1.0f, 1.0f, 0, 0, SOURCE_MOUSE,
+ 0, 0, CLASSIFICATION_MULTI_FINGER_SWIPE);
mMotionEventsHandler.onMotionEvent(down);
mMotionEventsHandler.onMotionEvent(move1);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
index 8440455..39c4e06 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
@@ -23,10 +23,14 @@
import android.os.UserManager
import android.test.suitebuilder.annotation.SmallTest
import androidx.test.runner.AndroidJUnit4
+import com.android.internal.logging.UiEventLogger
import com.android.systemui.SysuiTestCase
-import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.ACTION_CREATE_NOTE
+import com.android.systemui.notetask.NoteTaskController.Companion.INTENT_EXTRA_USE_STYLUS_MODE
+import com.android.systemui.notetask.NoteTaskController.ShowNoteTaskUiEvent
+import com.android.systemui.notetask.NoteTaskInfoResolver.NoteTaskInfo
import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.whenever
import com.android.wm.shell.bubbles.Bubbles
@@ -36,8 +40,8 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
-import org.mockito.Mockito.never
import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.MockitoAnnotations
/**
@@ -50,24 +54,23 @@
@RunWith(AndroidJUnit4::class)
internal class NoteTaskControllerTest : SysuiTestCase() {
- private val notesIntent = Intent(ACTION_CREATE_NOTE)
-
@Mock lateinit var context: Context
@Mock lateinit var packageManager: PackageManager
- @Mock lateinit var noteTaskIntentResolver: NoteTaskIntentResolver
+ @Mock lateinit var resolver: NoteTaskInfoResolver
@Mock lateinit var bubbles: Bubbles
@Mock lateinit var optionalBubbles: Optional<Bubbles>
@Mock lateinit var keyguardManager: KeyguardManager
@Mock lateinit var optionalKeyguardManager: Optional<KeyguardManager>
@Mock lateinit var optionalUserManager: Optional<UserManager>
@Mock lateinit var userManager: UserManager
+ @Mock lateinit var uiEventLogger: UiEventLogger
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
whenever(context.packageManager).thenReturn(packageManager)
- whenever(noteTaskIntentResolver.resolveIntent()).thenReturn(notesIntent)
+ whenever(resolver.resolveInfo()).thenReturn(NoteTaskInfo(NOTES_PACKAGE_NAME, NOTES_UID))
whenever(optionalBubbles.orElse(null)).thenReturn(bubbles)
whenever(optionalKeyguardManager.orElse(null)).thenReturn(keyguardManager)
whenever(optionalUserManager.orElse(null)).thenReturn(userManager)
@@ -77,101 +80,182 @@
private fun createNoteTaskController(isEnabled: Boolean = true): NoteTaskController {
return NoteTaskController(
context = context,
- intentResolver = noteTaskIntentResolver,
+ resolver = resolver,
optionalBubbles = optionalBubbles,
optionalKeyguardManager = optionalKeyguardManager,
optionalUserManager = optionalUserManager,
isEnabled = isEnabled,
+ uiEventLogger = uiEventLogger,
)
}
// region showNoteTask
@Test
- fun showNoteTask_keyguardIsLocked_shouldStartActivity() {
+ fun showNoteTask_keyguardIsLocked_shouldStartActivityAndLogUiEvent() {
whenever(keyguardManager.isKeyguardLocked).thenReturn(true)
- createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
+ createNoteTaskController()
+ .showNoteTask(
+ isInMultiWindowMode = false,
+ uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE,
+ )
- verify(context).startActivity(notesIntent)
- verify(bubbles, never()).showOrHideAppBubble(notesIntent)
+ val intentCaptor = argumentCaptor<Intent>()
+ verify(context).startActivity(capture(intentCaptor))
+ intentCaptor.value.let { intent ->
+ assertThat(intent.action).isEqualTo(NoteTaskController.ACTION_CREATE_NOTE)
+ assertThat(intent.`package`).isEqualTo(NOTES_PACKAGE_NAME)
+ assertThat(intent.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK)
+ assertThat(intent.getBooleanExtra(INTENT_EXTRA_USE_STYLUS_MODE, false)).isTrue()
+ }
+ verifyZeroInteractions(bubbles)
+ verify(uiEventLogger)
+ .log(
+ ShowNoteTaskUiEvent.NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE,
+ NOTES_UID,
+ NOTES_PACKAGE_NAME
+ )
}
@Test
- fun showNoteTask_keyguardIsUnlocked_shouldStartBubbles() {
+ fun showNoteTask_keyguardIsUnlocked_shouldStartBubblesAndLogUiEvent() {
whenever(keyguardManager.isKeyguardLocked).thenReturn(false)
- createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
+ createNoteTaskController()
+ .showNoteTask(
+ isInMultiWindowMode = false,
+ uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON,
+ )
- verify(bubbles).showOrHideAppBubble(notesIntent)
- verify(context, never()).startActivity(notesIntent)
+ verifyZeroInteractions(context)
+ val intentCaptor = argumentCaptor<Intent>()
+ verify(bubbles).showOrHideAppBubble(capture(intentCaptor))
+ intentCaptor.value.let { intent ->
+ assertThat(intent.action).isEqualTo(NoteTaskController.ACTION_CREATE_NOTE)
+ assertThat(intent.`package`).isEqualTo(NOTES_PACKAGE_NAME)
+ assertThat(intent.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK)
+ assertThat(intent.getBooleanExtra(INTENT_EXTRA_USE_STYLUS_MODE, false)).isTrue()
+ }
+ verify(uiEventLogger)
+ .log(
+ ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON,
+ NOTES_UID,
+ NOTES_PACKAGE_NAME
+ )
}
@Test
- fun showNoteTask_isInMultiWindowMode_shouldStartActivity() {
+ fun showNoteTask_keyguardIsUnlocked_uiEventIsNull_shouldStartBubblesWithoutLoggingUiEvent() {
whenever(keyguardManager.isKeyguardLocked).thenReturn(false)
- createNoteTaskController().showNoteTask(isInMultiWindowMode = true)
+ createNoteTaskController().showNoteTask(isInMultiWindowMode = false, uiEvent = null)
- verify(context).startActivity(notesIntent)
- verify(bubbles, never()).showOrHideAppBubble(notesIntent)
+ verifyZeroInteractions(context)
+ val intentCaptor = argumentCaptor<Intent>()
+ verify(bubbles).showOrHideAppBubble(capture(intentCaptor))
+ intentCaptor.value.let { intent ->
+ assertThat(intent.action).isEqualTo(NoteTaskController.ACTION_CREATE_NOTE)
+ assertThat(intent.`package`).isEqualTo(NOTES_PACKAGE_NAME)
+ assertThat(intent.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK)
+ assertThat(intent.getBooleanExtra(INTENT_EXTRA_USE_STYLUS_MODE, false)).isTrue()
+ }
+ verifyZeroInteractions(uiEventLogger)
+ }
+
+ @Test
+ fun showNoteTask_isInMultiWindowMode_shouldStartActivityAndLogUiEvent() {
+ whenever(keyguardManager.isKeyguardLocked).thenReturn(false)
+
+ createNoteTaskController()
+ .showNoteTask(
+ isInMultiWindowMode = true,
+ uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_SHORTCUT,
+ )
+
+ val intentCaptor = argumentCaptor<Intent>()
+ verify(context).startActivity(capture(intentCaptor))
+ intentCaptor.value.let { intent ->
+ assertThat(intent.action).isEqualTo(NoteTaskController.ACTION_CREATE_NOTE)
+ assertThat(intent.`package`).isEqualTo(NOTES_PACKAGE_NAME)
+ assertThat(intent.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK)
+ assertThat(intent.getBooleanExtra(INTENT_EXTRA_USE_STYLUS_MODE, false)).isTrue()
+ }
+ verifyZeroInteractions(bubbles)
+ verify(uiEventLogger)
+ .log(ShowNoteTaskUiEvent.NOTE_OPENED_VIA_SHORTCUT, NOTES_UID, NOTES_PACKAGE_NAME)
}
@Test
fun showNoteTask_bubblesIsNull_shouldDoNothing() {
whenever(optionalBubbles.orElse(null)).thenReturn(null)
- createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
+ createNoteTaskController()
+ .showNoteTask(
+ isInMultiWindowMode = false,
+ uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON
+ )
- verify(context, never()).startActivity(notesIntent)
- verify(bubbles, never()).showOrHideAppBubble(notesIntent)
+ verifyZeroInteractions(context, bubbles, uiEventLogger)
}
@Test
fun showNoteTask_keyguardManagerIsNull_shouldDoNothing() {
whenever(optionalKeyguardManager.orElse(null)).thenReturn(null)
- createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
+ createNoteTaskController()
+ .showNoteTask(
+ isInMultiWindowMode = false,
+ uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON,
+ )
- verify(context, never()).startActivity(notesIntent)
- verify(bubbles, never()).showOrHideAppBubble(notesIntent)
+ verifyZeroInteractions(context, bubbles, uiEventLogger)
}
@Test
fun showNoteTask_userManagerIsNull_shouldDoNothing() {
whenever(optionalUserManager.orElse(null)).thenReturn(null)
- createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
+ createNoteTaskController()
+ .showNoteTask(
+ isInMultiWindowMode = false,
+ uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON,
+ )
- verify(context, never()).startActivity(notesIntent)
- verify(bubbles, never()).showOrHideAppBubble(notesIntent)
+ verifyZeroInteractions(context, bubbles, uiEventLogger)
}
@Test
fun showNoteTask_intentResolverReturnsNull_shouldDoNothing() {
- whenever(noteTaskIntentResolver.resolveIntent()).thenReturn(null)
+ whenever(resolver.resolveInfo()).thenReturn(null)
- createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
+ createNoteTaskController()
+ .showNoteTask(
+ isInMultiWindowMode = false,
+ uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON,
+ )
- verify(context, never()).startActivity(notesIntent)
- verify(bubbles, never()).showOrHideAppBubble(notesIntent)
+ verifyZeroInteractions(context, bubbles, uiEventLogger)
}
@Test
fun showNoteTask_flagDisabled_shouldDoNothing() {
- createNoteTaskController(isEnabled = false).showNoteTask()
+ createNoteTaskController(isEnabled = false)
+ .showNoteTask(uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON)
- verify(context, never()).startActivity(notesIntent)
- verify(bubbles, never()).showOrHideAppBubble(notesIntent)
+ verifyZeroInteractions(context, bubbles, uiEventLogger)
}
@Test
fun showNoteTask_userIsLocked_shouldDoNothing() {
whenever(userManager.isUserUnlocked).thenReturn(false)
- createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
+ createNoteTaskController()
+ .showNoteTask(
+ isInMultiWindowMode = false,
+ uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON,
+ )
- verify(context, never()).startActivity(notesIntent)
- verify(bubbles, never()).showOrHideAppBubble(notesIntent)
+ verifyZeroInteractions(context, bubbles, uiEventLogger)
}
// endregion
@@ -206,4 +290,9 @@
assertThat(argument.value.flattenToString()).isEqualTo(expected.flattenToString())
}
// endregion
+
+ private companion object {
+ const val NOTES_PACKAGE_NAME = "com.android.note.app"
+ const val NOTES_UID = 123456
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoResolverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoResolverTest.kt
new file mode 100644
index 0000000..d6495d8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoResolverTest.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2023 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.systemui.notetask
+
+import android.app.role.RoleManager
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.test.suitebuilder.annotation.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.MockitoAnnotations
+
+/**
+ * Tests for [NoteTaskInfoResolver].
+ *
+ * Build/Install/Run:
+ * - atest SystemUITests:NoteTaskInfoResolverTest
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+internal class NoteTaskInfoResolverTest : SysuiTestCase() {
+
+ @Mock lateinit var packageManager: PackageManager
+ @Mock lateinit var roleManager: RoleManager
+
+ private lateinit var underTest: NoteTaskInfoResolver
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ underTest = NoteTaskInfoResolver(context, roleManager, packageManager)
+ }
+
+ @Test
+ fun resolveInfo_shouldReturnInfo() {
+ val packageName = "com.android.note.app"
+ val uid = 123456
+ whenever(roleManager.getRoleHoldersAsUser(NoteTaskInfoResolver.ROLE_NOTES, context.user))
+ .then { listOf(packageName) }
+ whenever(
+ packageManager.getApplicationInfoAsUser(
+ eq(packageName),
+ any<PackageManager.ApplicationInfoFlags>(),
+ eq(context.user)
+ )
+ )
+ .thenReturn(ApplicationInfo().apply { this.uid = uid })
+
+ val actual = underTest.resolveInfo()
+
+ requireNotNull(actual) { "Note task info must not be null" }
+ assertThat(actual.packageName).isEqualTo(packageName)
+ assertThat(actual.uid).isEqualTo(uid)
+ }
+
+ @Test
+ fun resolveInfo_packageManagerThrowsException_shouldReturnInfoWithZeroUid() {
+ val packageName = "com.android.note.app"
+ whenever(roleManager.getRoleHoldersAsUser(NoteTaskInfoResolver.ROLE_NOTES, context.user))
+ .then { listOf(packageName) }
+ whenever(
+ packageManager.getApplicationInfoAsUser(
+ eq(packageName),
+ any<PackageManager.ApplicationInfoFlags>(),
+ eq(context.user)
+ )
+ )
+ .thenThrow(PackageManager.NameNotFoundException(packageName))
+
+ val actual = underTest.resolveInfo()
+
+ requireNotNull(actual) { "Note task info must not be null" }
+ assertThat(actual.packageName).isEqualTo(packageName)
+ assertThat(actual.uid).isEqualTo(0)
+ }
+
+ @Test
+ fun resolveInfo_noRoleHolderIsSet_shouldReturnNull() {
+ whenever(roleManager.getRoleHoldersAsUser(eq(NoteTaskInfoResolver.ROLE_NOTES), any()))
+ .then { listOf<String>() }
+
+ val actual = underTest.resolveInfo()
+
+ assertThat(actual).isNull()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
index 010ac5b..53720ff 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
@@ -15,11 +15,14 @@
*/
package com.android.systemui.notetask
+import android.app.KeyguardManager
import android.test.suitebuilder.annotation.SmallTest
import android.view.KeyEvent
import androidx.test.runner.AndroidJUnit4
import com.android.systemui.SysuiTestCase
+import com.android.systemui.notetask.NoteTaskController.ShowNoteTaskUiEvent
import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.wm.shell.bubbles.Bubbles
import java.util.Optional
@@ -30,6 +33,7 @@
import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.MockitoAnnotations
/**
@@ -55,12 +59,16 @@
whenever(optionalBubbles.orElse(null)).thenReturn(bubbles)
}
- private fun createNoteTaskInitializer(isEnabled: Boolean = true): NoteTaskInitializer {
+ private fun createNoteTaskInitializer(
+ isEnabled: Boolean = true,
+ optionalKeyguardManager: Optional<KeyguardManager> = Optional.empty(),
+ ): NoteTaskInitializer {
return NoteTaskInitializer(
optionalBubbles = optionalBubbles,
noteTaskController = noteTaskController,
commandQueue = commandQueue,
isEnabled = isEnabled,
+ optionalKeyguardManager = optionalKeyguardManager,
)
}
@@ -105,19 +113,44 @@
// region handleSystemKey
@Test
- fun handleSystemKey_receiveValidSystemKey_shouldShowNoteTask() {
- createNoteTaskInitializer()
+ fun handleSystemKey_receiveValidSystemKey_keyguardNotLocked_shouldShowNoteTaskWithUnlocked() {
+ val keyguardManager =
+ mock<KeyguardManager>() { whenever(isKeyguardLocked).thenReturn(false) }
+ createNoteTaskInitializer(optionalKeyguardManager = Optional.of(keyguardManager))
.callbacks
.handleSystemKey(NoteTaskController.NOTE_TASK_KEY_EVENT)
- verify(noteTaskController).showNoteTask()
+ verify(noteTaskController)
+ .showNoteTask(uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON)
+ }
+
+ @Test
+ fun handleSystemKey_receiveValidSystemKey_keyguardLocked_shouldShowNoteTaskWithLocked() {
+ val keyguardManager =
+ mock<KeyguardManager>() { whenever(isKeyguardLocked).thenReturn(true) }
+ createNoteTaskInitializer(optionalKeyguardManager = Optional.of(keyguardManager))
+ .callbacks
+ .handleSystemKey(NoteTaskController.NOTE_TASK_KEY_EVENT)
+
+ verify(noteTaskController)
+ .showNoteTask(uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON_LOCKED)
+ }
+
+ @Test
+ fun handleSystemKey_receiveValidSystemKey_nullKeyguardManager_shouldShowNoteTaskWithUnlocked() {
+ createNoteTaskInitializer(optionalKeyguardManager = Optional.empty())
+ .callbacks
+ .handleSystemKey(NoteTaskController.NOTE_TASK_KEY_EVENT)
+
+ verify(noteTaskController)
+ .showNoteTask(uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON)
}
@Test
fun handleSystemKey_receiveInvalidSystemKey_shouldDoNothing() {
createNoteTaskInitializer().callbacks.handleSystemKey(KeyEvent.KEYCODE_UNKNOWN)
- verify(noteTaskController, never()).showNoteTask()
+ verifyZeroInteractions(noteTaskController)
}
// endregion
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt
deleted file mode 100644
index 18be92b..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright (C) 2022 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.systemui.notetask
-
-import android.app.role.RoleManager
-import android.content.Intent
-import android.content.pm.PackageManager
-import android.test.suitebuilder.annotation.SmallTest
-import androidx.test.runner.AndroidJUnit4
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.ACTION_CREATE_NOTE
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.whenever
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Mockito.any
-import org.mockito.MockitoAnnotations
-
-/**
- * Tests for [NoteTaskIntentResolver].
- *
- * Build/Install/Run:
- * - atest SystemUITests:NoteTaskIntentResolverTest
- */
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-internal class NoteTaskIntentResolverTest : SysuiTestCase() {
-
- @Mock lateinit var packageManager: PackageManager
- @Mock lateinit var roleManager: RoleManager
-
- private lateinit var underTest: NoteTaskIntentResolver
-
- @Before
- fun setUp() {
- MockitoAnnotations.initMocks(this)
- underTest = NoteTaskIntentResolver(context, roleManager)
- }
-
- @Test
- fun resolveIntent_shouldReturnIntentInStylusMode() {
- val packageName = "com.android.note.app"
- whenever(roleManager.getRoleHoldersAsUser(NoteTaskIntentResolver.ROLE_NOTES, context.user))
- .then { listOf(packageName) }
-
- val actual = underTest.resolveIntent()
-
- requireNotNull(actual) { "Intent must not be null" }
- assertThat(actual.action).isEqualTo(ACTION_CREATE_NOTE)
- assertThat(actual.`package`).isEqualTo(packageName)
- val expectedExtra = actual.getExtra(NoteTaskIntentResolver.INTENT_EXTRA_USE_STYLUS_MODE)
- assertThat(expectedExtra).isEqualTo(true)
- val expectedFlag = actual.flags and Intent.FLAG_ACTIVITY_NEW_TASK
- assertThat(expectedFlag).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK)
- }
-
- @Test
- fun resolveIntent_noRoleHolderIsSet_shouldReturnNull() {
- whenever(roleManager.getRoleHoldersAsUser(eq(NoteTaskIntentResolver.ROLE_NOTES), any()))
- .then { listOf<String>() }
-
- val actual = underTest.resolveIntent()
-
- assertThat(actual).isNull()
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt
index a1d42a0..cdc683f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt
@@ -27,7 +27,7 @@
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.LockScreenState
import com.android.systemui.notetask.NoteTaskController
-import com.android.systemui.util.mockito.whenever
+import com.android.systemui.notetask.NoteTaskController.ShowNoteTaskUiEvent
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
@@ -53,7 +53,6 @@
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
- whenever(noteTaskController.showNoteTask()).then {}
}
private fun createUnderTest(isEnabled: Boolean) =
@@ -96,6 +95,7 @@
underTest.onTriggered(expandable = null)
- verify(noteTaskController).showNoteTask()
+ verify(noteTaskController)
+ .showNoteTask(uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
index ccf378a..9312643 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
@@ -128,6 +128,11 @@
@Test
fun refreshUsers() = runSelfCancelingTest {
+ val mainUserId = 10
+ val mainUser = mock(UserHandle::class.java)
+ whenever(manager.mainUser).thenReturn(mainUser)
+ whenever(mainUser.identifier).thenReturn(mainUserId)
+
underTest = create(this)
val initialExpectedValue =
setUpUsers(
@@ -166,6 +171,7 @@
assertThat(selectedUserInfo).isEqualTo(thirdExpectedValue[1])
assertThat(selectedUserInfo?.isGuest).isTrue()
assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedNonGuestUserId)
+ assertThat(underTest.mainUserId).isEqualTo(mainUserId)
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt
index fb781e8..cc23485 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt
@@ -204,10 +204,12 @@
}
@Test
- fun `exit - last non-guest was removed - returns to system`() =
+ fun `exit - last non-guest was removed - returns to main user`() =
runBlocking(IMMEDIATE) {
val removedUserId = 310
+ val mainUserId = 10
repository.lastSelectedNonGuestUserId = removedUserId
+ repository.mainUserId = mainUserId
repository.setSelectedUserInfo(GUEST_USER_INFO)
underTest.exit(
@@ -221,7 +223,7 @@
verify(manager, never()).markGuestForDeletion(anyInt())
verify(manager, never()).removeUser(anyInt())
- verify(switchUser).invoke(UserHandle.USER_SYSTEM)
+ verify(switchUser).invoke(mainUserId)
}
@Test
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
index 1a8e244..53bb340 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
@@ -28,6 +28,10 @@
import kotlinx.coroutines.yield
class FakeUserRepository : UserRepository {
+ companion object {
+ // User id to represent a non system (human) user id. We presume this is the main user.
+ private const val MAIN_USER_ID = 10
+ }
private val _userSwitcherSettings = MutableStateFlow(UserSwitcherSettingsModel())
override val userSwitcherSettings: Flow<UserSwitcherSettingsModel> =
@@ -43,7 +47,8 @@
override val userSwitchingInProgress: Flow<Boolean>
get() = _userSwitchingInProgress
- override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM
+ override var mainUserId: Int = MAIN_USER_ID
+ override var lastSelectedNonGuestUserId: Int = mainUserId
private var _isGuestUserAutoCreated: Boolean = false
override val isGuestUserAutoCreated: Boolean
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index ce6a6c8..56d0b59 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -20,6 +20,9 @@
import static android.Manifest.permission.REQUEST_COMPANION_START_FOREGROUND_SERVICES_FROM_BACKGROUND;
import static android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND;
import static android.Manifest.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND;
+import static android.app.ActivityManager.PROCESS_CAPABILITY_BFSL;
+import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+import static android.app.ActivityManager.PROCESS_STATE_BOUND_TOP;
import static android.app.ActivityManager.PROCESS_STATE_HEAVY_WEIGHT;
import static android.app.ActivityManager.PROCESS_STATE_RECEIVER;
import static android.app.ActivityManager.PROCESS_STATE_TOP;
@@ -7395,6 +7398,12 @@
int ret = shouldAllowFgsStartForegroundNoBindingCheckLocked(allowWhileInUse, callingPid,
callingUid, callingPackage, r, backgroundStartPrivileges);
+ // If an app (App 1) is bound by another app (App 2) that could start an FGS, then App 1
+ // is also allowed to start an FGS. We check all the binding
+ // in canBindingClientStartFgsLocked() to do this check.
+ // (Note we won't check more than 1 level of binding.)
+ // [bookmark: 61867f60-007c-408c-a2c4-e19e96056135] -- this code is referred to from
+ // OomAdjuster.
String bindFromPackage = null;
if (ret == REASON_DENIED) {
bindFromPackage = canBindingClientStartFgsLocked(callingUid);
@@ -7410,10 +7419,13 @@
.getTargetSdkVersion(callingPackage);
} catch (PackageManager.NameNotFoundException ignored) {
}
+ final boolean uidBfsl = (mAm.getUidProcessCapabilityLocked(callingUid)
+ & PROCESS_CAPABILITY_BFSL) != 0;
final String debugInfo =
"[callingPackage: " + callingPackage
+ "; callingUid: " + callingUid
+ "; uidState: " + ProcessList.makeProcStateString(uidState)
+ + "; uidBFSL: " + (uidBfsl ? "[BFSL]" : "n/a")
+ "; intent: " + intent
+ "; code:" + reasonCodeToString(ret)
+ "; tempAllowListReason:<"
@@ -7452,11 +7464,15 @@
}
if (ret == REASON_DENIED) {
+ final boolean uidBfsl =
+ (mAm.getUidProcessCapabilityLocked(callingUid) & PROCESS_CAPABILITY_BFSL) != 0;
final Integer allowedType = mAm.mProcessList.searchEachLruProcessesLOSP(false, app -> {
if (app.uid == callingUid) {
final ProcessStateRecord state = app.mState;
- if (state.isAllowedStartFgs()) { // Procstate <= BFGS?
- return getReasonCodeFromProcState(state.getCurProcState());
+ final int procstate = state.getCurProcState();
+ if ((procstate <= PROCESS_STATE_BOUND_TOP)
+ || (uidBfsl && (procstate <= PROCESS_STATE_BOUND_FOREGROUND_SERVICE))) {
+ return getReasonCodeFromProcState(procstate);
} else {
final ActiveInstrumentation instr = app.getActiveInstrumentation();
if (instr != null
@@ -7684,6 +7700,8 @@
}
final int callerTargetSdkVersion = r.mRecentCallerApplicationInfo != null
? r.mRecentCallerApplicationInfo.targetSdkVersion : 0;
+
+ // TODO(short-service): Log BFSL too.
FrameworkStatsLog.write(FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED,
r.appInfo.uid,
r.shortInstanceName,
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index b272502..aad49c9 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -5874,6 +5874,11 @@
return mProcessList.getUidProcStateLOSP(uid);
}
+ @GuardedBy("this")
+ int getUidProcessCapabilityLocked(int uid) {
+ return mProcessList.getUidProcessCapabilityLOSP(uid);
+ }
+
// =========================================================
// PROCESS INFO
// =========================================================
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index 3ab1cd7..f855c7f 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -16,6 +16,10 @@
package com.android.server.am;
+import static android.app.ActivityManager.PROCESS_CAPABILITY_FOREGROUND_CAMERA;
+import static android.app.ActivityManager.PROCESS_CAPABILITY_FOREGROUND_LOCATION;
+import static android.app.ActivityManager.PROCESS_CAPABILITY_FOREGROUND_MICROPHONE;
+import static android.app.ActivityManager.PROCESS_CAPABILITY_NETWORK;
import static android.app.ActivityManagerInternal.ALLOW_NON_FULL;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.app.ActivityTaskManager.RESIZE_MODE_SYSTEM;
@@ -1823,16 +1827,20 @@
final InputStream mInput;
final int mUid;
+ final int mMask;
+
static final int STATE_NORMAL = 0;
int mState;
- MyUidObserver(ActivityManagerService service, PrintWriter pw, InputStream input, int uid) {
+ MyUidObserver(ActivityManagerService service, PrintWriter pw, InputStream input, int uid,
+ int mask) {
mInterface = service;
mInternal = service;
mPw = pw;
mInput = input;
mUid = uid;
+ mMask = mask;
}
@Override
@@ -1847,7 +1855,7 @@
mPw.print(" seq ");
mPw.print(procStateSeq);
mPw.print(" capability ");
- mPw.println(capability);
+ mPw.println(capability & mMask);
mPw.flush();
} finally {
StrictMode.setThreadPolicy(oldPolicy);
@@ -1998,9 +2006,19 @@
int runWatchUids(PrintWriter pw) throws RemoteException {
String opt;
int uid = -1;
+
+ // Because a lot of CTS won't ignore capabilities newly added, we report
+ // only the following capabilities -- the ones we had on Android T -- by default.
+ int mask = PROCESS_CAPABILITY_FOREGROUND_LOCATION
+ | PROCESS_CAPABILITY_FOREGROUND_CAMERA
+ | PROCESS_CAPABILITY_FOREGROUND_MICROPHONE
+ | PROCESS_CAPABILITY_NETWORK;
+
while ((opt=getNextOption()) != null) {
if (opt.equals("--oom")) {
uid = Integer.parseInt(getNextArgRequired());
+ } else if (opt.equals("--mask")) {
+ mask = Integer.parseInt(getNextArgRequired());
} else {
getErrPrintWriter().println("Error: Unknown option: " + opt);
return -1;
@@ -2008,7 +2026,7 @@
}
}
- MyUidObserver controller = new MyUidObserver(mInternal, pw, getRawInputStream(), uid);
+ MyUidObserver controller = new MyUidObserver(mInternal, pw, getRawInputStream(), uid, mask);
controller.run();
return 0;
}
@@ -4079,9 +4097,14 @@
pw.println(" -p: only show events related to a specific process / package");
pw.println(" -s: simple mode, only show a summary line for each event");
pw.println(" -c: assume the input is always [c]ontinue");
- pw.println(" watch-uids [--oom <uid>]");
+ pw.println(" watch-uids [--oom <uid>] [--mask <capabilities integer>]");
pw.println(" Start watching for and reporting uid state changes.");
pw.println(" --oom: specify a uid for which to report detailed change messages.");
+ pw.println(" --mask: Specify PROCESS_CAPABILITY_XXX mask to report. ");
+ pw.println(" By default, it only reports FOREGROUND_LOCATION (1)");
+ pw.println(" FOREGROUND_CAMERA (2), FOREGROUND_MICROPHONE (4)");
+ pw.println(" and NETWORK (8). New capabilities added on or after");
+ pw.println(" Android UDC will not be reported by default.");
pw.println(" hang [--allow-restart]");
pw.println(" Hang the system.");
pw.println(" --allow-restart: allow watchdog to perform normal system restart");
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index 3643db0..8591973 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -18,6 +18,7 @@
import static android.app.ActivityManager.PROCESS_CAPABILITY_ALL;
import static android.app.ActivityManager.PROCESS_CAPABILITY_ALL_IMPLICIT;
+import static android.app.ActivityManager.PROCESS_CAPABILITY_BFSL;
import static android.app.ActivityManager.PROCESS_CAPABILITY_FOREGROUND_CAMERA;
import static android.app.ActivityManager.PROCESS_CAPABILITY_FOREGROUND_LOCATION;
import static android.app.ActivityManager.PROCESS_CAPABILITY_FOREGROUND_MICROPHONE;
@@ -1706,7 +1707,7 @@
state.setCurRawAdj(state.getMaxAdj());
state.setHasForegroundActivities(false);
state.setCurrentSchedulingGroup(SCHED_GROUP_DEFAULT);
- state.setCurCapability(PROCESS_CAPABILITY_ALL);
+ state.setCurCapability(PROCESS_CAPABILITY_ALL); // BFSL allowed
state.setCurProcState(ActivityManager.PROCESS_STATE_PERSISTENT);
// System processes can do UI, and when they do we want to have
// them trim their memory after the user leaves the UI. To
@@ -1788,6 +1789,7 @@
schedGroup = SCHED_GROUP_DEFAULT;
state.setAdjType("instrumentation");
procState = PROCESS_STATE_FOREGROUND_SERVICE;
+ capability |= PROCESS_CAPABILITY_BFSL;
if (DEBUG_OOM_ADJ_REASON || logUid == appUid) {
reportOomAdjMessageLocked(TAG_OOM_ADJ, "Making instrumentation: " + app);
}
@@ -1880,37 +1882,28 @@
adjType = "fg-service";
newAdj = PERCEPTIBLE_APP_ADJ;
newProcState = PROCESS_STATE_FOREGROUND_SERVICE;
+ capabilityFromFGS |= PROCESS_CAPABILITY_BFSL;
+
+ } else if (hasShortForegroundServices) {
+
+ // For short FGS.
+ adjType = "fg-service-short";
+
+ // We use MEDIUM_APP_ADJ + 1 so we can tell apart EJ
+ // (which uses MEDIUM_APP_ADJ + 1)
+ // from short-FGS.
+ // (We use +1 and +2, not +0 and +1, to be consistent with the following
+ // RECENT_FOREGROUND_APP_ADJ tweak)
+ newAdj = PERCEPTIBLE_MEDIUM_APP_ADJ + 1;
+
+ // We give the FGS procstate, but not PROCESS_CAPABILITY_BFSL, so
+ // short-fgs can't start FGS from the background.
+ newProcState = PROCESS_STATE_FOREGROUND_SERVICE;
} else if (state.hasOverlayUi()) {
adjType = "has-overlay-ui";
newAdj = PERCEPTIBLE_APP_ADJ;
newProcState = PROCESS_STATE_IMPORTANT_FOREGROUND;
-
- } else if (hasForegroundServices) {
- // If we get here, hasNonShortForegroundServices() must be false.
-
- // TODO(short-service): Proactively run OomAjudster when the grace period finish.
- if (!hasShortForegroundServices) {
- // All the short-FGSes within this process are timed out. Don't promote to FGS.
- // TODO(short-service): Should we set some unique oom-adj to make it detectable,
- // in a long trace?
- } else {
- // For short FGS.
- adjType = "fg-service-short";
- // We use MEDIUM_APP_ADJ + 1 so we can tell apart EJ
- // (which uses MEDIUM_APP_ADJ + 2)
- // from short-FGS.
- // (We use +1 and +2, not +0 and +1, to be consistent with the following
- // RECENT_FOREGROUND_APP_ADJ tweak)
- newAdj = PERCEPTIBLE_MEDIUM_APP_ADJ + 1;
-
- // Short-FGS gets a below-BFGS procstate, so it can't start another FGS from it.
- newProcState = PROCESS_STATE_IMPORTANT_FOREGROUND;
-
- // Same as EJ, we explicitly grant network access to short FGS,
- // even when battery saver or data saver is enabled.
- capabilityFromFGS |= PROCESS_CAPABILITY_NETWORK;
- }
}
if (adjType != null) {
@@ -2220,6 +2213,11 @@
app.mOptRecord.setShouldNotFreeze(true);
}
+ // We always propagate PROCESS_CAPABILITY_BFSL over bindings here,
+ // but, right before actually setting it to the process,
+ // we check the final procstate, and remove it if the procsate is below BFGS.
+ capability |= getBfslCapabilityFromClient(client);
+
if ((cr.flags & Context.BIND_WAIVE_PRIORITY) == 0) {
if (cr.hasFlag(Context.BIND_INCLUDE_CAPABILITIES)) {
capability |= cstate.getCurCapability();
@@ -2540,6 +2538,11 @@
int clientAdj = cstate.getCurRawAdj();
int clientProcState = cstate.getCurRawProcState();
+ // We always propagate PROCESS_CAPABILITY_BFSL to providers here,
+ // but, right before actually setting it to the process,
+ // we check the final procstate, and remove it if the procsate is below BFGS.
+ capability |= getBfslCapabilityFromClient(client);
+
if (clientProcState >= PROCESS_STATE_CACHED_ACTIVITY) {
// If the other app is cached for any reason, for purposes here
// we are going to consider it empty.
@@ -2718,6 +2721,11 @@
capability |= getDefaultCapability(app, procState);
+ // Procstates below BFGS should never have this capability.
+ if (procState > PROCESS_STATE_BOUND_FOREGROUND_SERVICE) {
+ capability &= ~PROCESS_CAPABILITY_BFSL;
+ }
+
// Do final modification to adj. Everything we do between here and applying
// the final setAdj must be done in this function, because we will also use
// it when computing the final cached adj later. Note that we don't need to
@@ -2743,9 +2751,9 @@
case PROCESS_STATE_PERSISTENT:
case PROCESS_STATE_PERSISTENT_UI:
case PROCESS_STATE_TOP:
- return PROCESS_CAPABILITY_ALL;
+ return PROCESS_CAPABILITY_ALL; // BFSL allowed
case PROCESS_STATE_BOUND_TOP:
- return PROCESS_CAPABILITY_NETWORK;
+ return PROCESS_CAPABILITY_NETWORK | PROCESS_CAPABILITY_BFSL;
case PROCESS_STATE_FOREGROUND_SERVICE:
if (app.getActiveInstrumentation() != null) {
return PROCESS_CAPABILITY_ALL_IMPLICIT | PROCESS_CAPABILITY_NETWORK ;
@@ -2763,6 +2771,53 @@
}
/**
+ * @return the BFSL capability from a client (of a service binding or provider).
+ */
+ int getBfslCapabilityFromClient(ProcessRecord client) {
+ // Procstates above FGS should always have this flag. We shouldn't need this logic,
+ // but let's do it just in case.
+ if (client.mState.getCurProcState() < PROCESS_STATE_FOREGROUND_SERVICE) {
+ return PROCESS_CAPABILITY_BFSL;
+ }
+ // Otherwise, use the process's cur capability.
+
+ // Note: BFSL is a per-UID check, not per-process, but here, the BFSL capability is still
+ // propagated on a per-process basis.
+ //
+ // For example, consider this case:
+ // - There are App 1 and App 2.
+ // - App 1 has two processes
+ // Proc #1A, procstate BFGS with CAPABILITY_BFSL
+ // Proc #1B, procstate FGS with no CAPABILITY_BFSL (i.e. process has a short FGS)
+ // And this process binds to Proc #2 of App 2.
+ //
+ // (Note because #1A has CAPABILITY_BFSL, App 1's UidRecord has CAPABILITY_BFSL.)
+ //
+ // - App 2 has one process:
+ // Proc #2, procstate FGS due to the above binding, _with no CAPABILITY_BFSL_.
+ //
+ // In this case, #2 will not get CAPABILITY_BFSL because the binding client (#1B)
+ // doesn't have this capability. (Even though App 1's UidRecord has it.)
+ //
+ // This may look weird, because App 2 _is_ still BFSL allowed, because "it's bound by
+ // an app that is BFSL-allowed". (See [bookmark: 61867f60-007c-408c-a2c4-e19e96056135]
+ // in ActiveServices.)
+ //
+ // So why don't we propagate PROCESS_CAPABILITY_BFSL from App 1's UID record?
+ // This is because short-FGS acts like "below BFGS" as far as BFSL is concerned,
+ // similar to how JobScheduler jobs are below BFGS and apps can't start FGS from there.
+ //
+ // If #1B was running a job instead of a short-FGS, then its procstate would be below BFGS.
+ // Then #2's procstate would also be below BFGS. So #2 wouldn't get CAPABILITY_BFSL.
+ // Similarly, if #1B has a short FGS, even though the procstate of #1B and #2 would be FGS,
+ // they both still wouldn't get CAPABILITY_BFSL.
+ //
+ // However, again, because #2 is bound by App 1, which is BFSL-allowed (because of #1A)
+ // App 2 would still BFSL-allowed, due to the aforementioned check in ActiveServices.
+ return client.mState.getCurCapability() & PROCESS_CAPABILITY_BFSL;
+ }
+
+ /**
* Checks if for the given app and client, there's a cycle that should skip over the client
* for now or use partial values to evaluate the effect of the client binding.
* @param app
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index 04c0d64..2b2de81e 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -3888,7 +3888,7 @@
return runList;
}
- @GuardedBy(anyOf = {"mService", "mProfileLock"})
+ @GuardedBy(anyOf = {"mService", "mProcLock"})
int getLruSizeLOSP() {
return mLruProcesses.size();
}
@@ -3896,7 +3896,7 @@
/**
* Return the reference to the LRU list, call this function for read-only access
*/
- @GuardedBy(anyOf = {"mService", "mProfileLock"})
+ @GuardedBy(anyOf = {"mService", "mProcLock"})
ArrayList<ProcessRecord> getLruProcessesLOSP() {
return mLruProcesses;
}
@@ -3904,7 +3904,7 @@
/**
* Return the reference to the LRU list, call this function for read/write access
*/
- @GuardedBy({"mService", "mProfileLock"})
+ @GuardedBy({"mService", "mProcLock"})
ArrayList<ProcessRecord> getLruProcessesLSP() {
return mLruProcesses;
}
@@ -3913,12 +3913,12 @@
* For test only
*/
@VisibleForTesting
- @GuardedBy({"mService", "mProfileLock"})
+ @GuardedBy({"mService", "mProcLock"})
void setLruProcessServiceStartLSP(int pos) {
mLruProcessServiceStart = pos;
}
- @GuardedBy(anyOf = {"mService", "mProfileLock"})
+ @GuardedBy(anyOf = {"mService", "mProcLock"})
int getLruProcessServiceStartLOSP() {
return mLruProcessServiceStart;
}
@@ -3931,7 +3931,7 @@
* to most recent used ProcessRecord.
* @param callback The callback interface to accept the current ProcessRecord.
*/
- @GuardedBy(anyOf = {"mService", "mProfileLock"})
+ @GuardedBy(anyOf = {"mService", "mProcLock"})
void forEachLruProcessesLOSP(boolean iterateForward,
@NonNull Consumer<ProcessRecord> callback) {
if (iterateForward) {
@@ -3956,7 +3956,7 @@
* a non-null object, the search will be halted and this object will be used
* as the return value of this search function.
*/
- @GuardedBy(anyOf = {"mService", "mProfileLock"})
+ @GuardedBy(anyOf = {"mService", "mProcLock"})
<R> R searchEachLruProcessesLOSP(boolean iterateForward,
@NonNull Function<ProcessRecord, R> callback) {
if (iterateForward) {
@@ -3977,17 +3977,17 @@
return null;
}
- @GuardedBy(anyOf = {"mService", "mProfileLock"})
+ @GuardedBy(anyOf = {"mService", "mProcLock"})
boolean isInLruListLOSP(ProcessRecord app) {
return mLruProcesses.contains(app);
}
- @GuardedBy(anyOf = {"mService", "mProfileLock"})
+ @GuardedBy(anyOf = {"mService", "mProcLock"})
int getLruSeqLOSP() {
return mLruSeq;
}
- @GuardedBy(anyOf = {"mService", "mProfileLock"})
+ @GuardedBy(anyOf = {"mService", "mProcLock"})
MyProcessMap getProcessNamesLOSP() {
return mProcessNames;
}
diff --git a/services/core/java/com/android/server/am/ProcessStateRecord.java b/services/core/java/com/android/server/am/ProcessStateRecord.java
index 2ad2077..71d5d39 100644
--- a/services/core/java/com/android/server/am/ProcessStateRecord.java
+++ b/services/core/java/com/android/server/am/ProcessStateRecord.java
@@ -17,7 +17,6 @@
package com.android.server.am;
import static android.app.ActivityManager.PROCESS_CAPABILITY_NONE;
-import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
import static android.app.ActivityManager.PROCESS_STATE_NONEXISTENT;
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_OOM_ADJ;
@@ -1180,11 +1179,6 @@
}
@GuardedBy("mService")
- boolean isAllowedStartFgs() {
- return mCurProcState <= PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
- }
-
- @GuardedBy("mService")
boolean isBackgroundRestricted() {
return mBackgroundRestricted;
}
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 12784bf..60a7f93 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -104,6 +104,7 @@
DeviceConfig.NAMESPACE_VENDOR_SYSTEM_NATIVE_BOOT,
DeviceConfig.NAMESPACE_VIRTUALIZATION_FRAMEWORK_NATIVE,
DeviceConfig.NAMESPACE_WINDOW_MANAGER_NATIVE_BOOT,
+ DeviceConfig.NAMESPACE_MEMORY_SAFETY_NATIVE_BOOT,
DeviceConfig.NAMESPACE_MEMORY_SAFETY_NATIVE,
DeviceConfig.NAMESPACE_HDMI_CONTROL
};
diff --git a/services/core/java/com/android/server/am/UidRecord.java b/services/core/java/com/android/server/am/UidRecord.java
index b617582..bfc022b 100644
--- a/services/core/java/com/android/server/am/UidRecord.java
+++ b/services/core/java/com/android/server/am/UidRecord.java
@@ -443,6 +443,8 @@
sb.append(",");
sb.append(lastNetworkUpdatedProcStateSeq);
sb.append(")}");
+ sb.append(" caps=");
+ ActivityManager.printCapabilitiesSummary(sb, mCurCapability);
return sb.toString();
}
}
diff --git a/services/core/java/com/android/server/display/DisplayModeDirector.java b/services/core/java/com/android/server/display/DisplayModeDirector.java
index 29caefb..91ef167 100644
--- a/services/core/java/com/android/server/display/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/DisplayModeDirector.java
@@ -733,7 +733,7 @@
/**
* Sets the display mode switching type.
- * @param newType
+ * @param newType new mode switching type
*/
public void setModeSwitchingType(@DisplayManager.SwitchingType int newType) {
synchronized (mLock) {
@@ -850,6 +850,18 @@
notifyDesiredDisplayModeSpecsChangedLocked();
}
+ @GuardedBy("mLock")
+ private float getMaxRefreshRateLocked(int displayId) {
+ Display.Mode[] modes = mSupportedModesByDisplay.get(displayId);
+ float maxRefreshRate = 0f;
+ for (Display.Mode mode : modes) {
+ if (mode.getRefreshRate() > maxRefreshRate) {
+ maxRefreshRate = mode.getRefreshRate();
+ }
+ }
+ return maxRefreshRate;
+ }
+
private void notifyDesiredDisplayModeSpecsChangedLocked() {
if (mDesiredDisplayModeSpecsListener != null
&& !mHandler.hasMessages(MSG_REFRESH_RATE_RANGE_CHANGED)) {
@@ -1186,29 +1198,33 @@
// rest of low priority voters. It votes [0, max(PEAK, MIN)]
public static final int PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE = 7;
+ // To avoid delay in switching between 60HZ -> 90HZ when activating LHBM, set refresh
+ // rate to max value (same as for PRIORITY_UDFPS) on lock screen
+ public static final int PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE = 8;
+
// For concurrent displays we want to limit refresh rate on all displays
- public static final int PRIORITY_LAYOUT_LIMITED_FRAME_RATE = 8;
+ public static final int PRIORITY_LAYOUT_LIMITED_FRAME_RATE = 9;
// LOW_POWER_MODE force the render frame rate to [0, 60HZ] if
// Settings.Global.LOW_POWER_MODE is on.
- public static final int PRIORITY_LOW_POWER_MODE = 9;
+ public static final int PRIORITY_LOW_POWER_MODE = 10;
// PRIORITY_FLICKER_REFRESH_RATE_SWITCH votes for disabling refresh rate switching. If the
// higher priority voters' result is a range, it will fix the rate to a single choice.
// It's used to avoid refresh rate switches in certain conditions which may result in the
// user seeing the display flickering when the switches occur.
- public static final int PRIORITY_FLICKER_REFRESH_RATE_SWITCH = 10;
+ public static final int PRIORITY_FLICKER_REFRESH_RATE_SWITCH = 11;
// Force display to [0, 60HZ] if skin temperature is at or above CRITICAL.
- public static final int PRIORITY_SKIN_TEMPERATURE = 11;
+ public static final int PRIORITY_SKIN_TEMPERATURE = 12;
// The proximity sensor needs the refresh rate to be locked in order to function, so this is
// set to a high priority.
- public static final int PRIORITY_PROXIMITY = 12;
+ public static final int PRIORITY_PROXIMITY = 13;
// The Under-Display Fingerprint Sensor (UDFPS) needs the refresh rate to be locked in order
// to function, so this needs to be the highest priority of all votes.
- public static final int PRIORITY_UDFPS = 13;
+ public static final int PRIORITY_UDFPS = 14;
// Whenever a new priority is added, remember to update MIN_PRIORITY, MAX_PRIORITY, and
// APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF, as well as priorityToString.
@@ -1325,6 +1341,8 @@
return "PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE";
case PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE:
return "PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE";
+ case PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE:
+ return "PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE";
default:
return Integer.toString(priority);
}
@@ -2564,6 +2582,7 @@
private class UdfpsObserver extends IUdfpsRefreshRateRequestCallback.Stub {
private final SparseBooleanArray mUdfpsRefreshRateEnabled = new SparseBooleanArray();
+ private final SparseBooleanArray mAuthenticationPossible = new SparseBooleanArray();
public void observe() {
StatusBarManagerInternal statusBar =
@@ -2576,38 +2595,38 @@
@Override
public void onRequestEnabled(int displayId) {
synchronized (mLock) {
- updateRefreshRateStateLocked(displayId, true /*enabled*/);
+ mUdfpsRefreshRateEnabled.put(displayId, true);
+ updateVoteLocked(displayId, true, Vote.PRIORITY_UDFPS);
}
}
@Override
public void onRequestDisabled(int displayId) {
synchronized (mLock) {
- updateRefreshRateStateLocked(displayId, false /*enabled*/);
+ mUdfpsRefreshRateEnabled.put(displayId, false);
+ updateVoteLocked(displayId, false, Vote.PRIORITY_UDFPS);
}
}
- private void updateRefreshRateStateLocked(int displayId, boolean enabled) {
- mUdfpsRefreshRateEnabled.put(displayId, enabled);
- updateVoteLocked(displayId);
+ @Override
+ public void onAuthenticationPossible(int displayId, boolean isPossible) {
+ synchronized (mLock) {
+ mAuthenticationPossible.put(displayId, isPossible);
+ updateVoteLocked(displayId, isPossible,
+ Vote.PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE);
+ }
}
- private void updateVoteLocked(int displayId) {
+ @GuardedBy("mLock")
+ private void updateVoteLocked(int displayId, boolean enabled, int votePriority) {
final Vote vote;
- if (mUdfpsRefreshRateEnabled.get(displayId)) {
- Display.Mode[] modes = mSupportedModesByDisplay.get(displayId);
- float maxRefreshRate = 0f;
- for (Display.Mode mode : modes) {
- if (mode.getRefreshRate() > maxRefreshRate) {
- maxRefreshRate = mode.getRefreshRate();
- }
- }
+ if (enabled) {
+ float maxRefreshRate = DisplayModeDirector.this.getMaxRefreshRateLocked(displayId);
vote = Vote.forPhysicalRefreshRates(maxRefreshRate, maxRefreshRate);
} else {
vote = null;
}
-
- DisplayModeDirector.this.updateVoteLocked(displayId, Vote.PRIORITY_UDFPS, vote);
+ DisplayModeDirector.this.updateVoteLocked(displayId, votePriority, vote);
}
void dumpLocked(PrintWriter pw) {
@@ -2618,6 +2637,13 @@
final String enabled = mUdfpsRefreshRateEnabled.valueAt(i) ? "enabled" : "disabled";
pw.println(" Display " + displayId + ": " + enabled);
}
+ pw.println(" mAuthenticationPossible: ");
+ for (int i = 0; i < mAuthenticationPossible.size(); i++) {
+ final int displayId = mAuthenticationPossible.keyAt(i);
+ final String isPossible = mAuthenticationPossible.valueAt(i) ? "possible"
+ : "impossible";
+ pw.println(" Display " + displayId + ": " + isPossible);
+ }
}
}
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index cd79f2e..e1fdeca1 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -32,6 +32,7 @@
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.BackgroundStartPrivileges;
+import android.app.ComponentOptions;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.PackageManager;
@@ -166,7 +167,8 @@
final boolean useCallingUidState =
originatingPendingIntent == null
|| checkedOptions == null
- || !checkedOptions.getIgnorePendingIntentCreatorForegroundState();
+ || checkedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
+ != ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED;
if (useCallingUidState) {
if (callingUid == Process.ROOT_UID
|| callingAppId == Process.SYSTEM_UID
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 2343906..110cce2 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -385,9 +385,9 @@
}
ProtoLog.d(WM_DEBUG_TASKS, "Comparing existing cls=%s /aff=%s to new cls=%s /aff=%s",
- r.getTask().rootAffinity, mIntent.getComponent().flattenToShortString(),
- mInfo.taskAffinity, (task.realActivity != null
- ? task.realActivity.flattenToShortString() : ""));
+ (task.realActivity != null ? task.realActivity.flattenToShortString() : ""),
+ task.rootAffinity, mIntent.getComponent().flattenToShortString(),
+ mTaskAffinity);
// TODO Refactor to remove duplications. Check if logic can be simplified.
if (task.realActivity != null && task.realActivity.compareTo(cls) == 0
&& Objects.equals(documentData, taskDocumentData)) {
diff --git a/services/core/java/com/android/server/wm/SafeActivityOptions.java b/services/core/java/com/android/server/wm/SafeActivityOptions.java
index 98ca9ae..5860776 100644
--- a/services/core/java/com/android/server/wm/SafeActivityOptions.java
+++ b/services/core/java/com/android/server/wm/SafeActivityOptions.java
@@ -146,8 +146,8 @@
.setLaunchRootTask(options.getLaunchRootTask())
.setPendingIntentBackgroundActivityStartMode(
options.getPendingIntentBackgroundActivityStartMode())
- .setIgnorePendingIntentCreatorForegroundState(
- options.getIgnorePendingIntentCreatorForegroundState());
+ .setPendingIntentCreatorBackgroundActivityStartMode(
+ options.getPendingIntentCreatorBackgroundActivityStartMode());
}
/**
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index f0c099f..04a2761 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -216,6 +216,12 @@
final TransitionController.Logger mLogger = new TransitionController.Logger();
+ /**
+ * {@code false} if this transition runs purely in WMCore (meaning Shell is completely unaware
+ * of it). Currently, this happens before the display is ready since nothing can be seen yet.
+ */
+ boolean mIsPlayerEnabled = true;
+
Transition(@TransitionType int type, @TransitionFlags int flags,
TransitionController controller, BLASTSyncEngine syncEngine) {
mType = type;
@@ -777,7 +783,7 @@
* be called directly; use {@link TransitionController#finishTransition} instead.
*/
void finishTransition() {
- if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
+ if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER) && mIsPlayerEnabled) {
Trace.asyncTraceEnd(TRACE_TAG_WINDOW_MANAGER, TRACE_NAME_PLAY_TRANSITION,
System.identityHashCode(this));
}
@@ -1112,7 +1118,7 @@
controller.setupStartTransaction(transaction);
}
buildFinishTransaction(mFinishTransaction, info.getRootLeash());
- if (mController.getTransitionPlayer() != null) {
+ if (mController.getTransitionPlayer() != null && mIsPlayerEnabled) {
mController.dispatchLegacyAppTransitionStarting(info, mStatusBarTransitionDelay);
try {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
@@ -1128,11 +1134,16 @@
} catch (RemoteException e) {
// If there's an exception when trying to send the mergedTransaction to the
// client, we should finish and apply it here so the transactions aren't lost.
- cleanUpOnFailure();
+ postCleanupOnFailure();
}
} else {
- // No player registered, so just finish/apply immediately
- cleanUpOnFailure();
+ // No player registered or it's not enabled, so just finish/apply immediately
+ if (!mIsPlayerEnabled) {
+ mLogger.mSendTimeNs = SystemClock.uptimeNanos();
+ ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Apply and finish immediately"
+ + " because player is disabled for transition #%d .", mSyncId);
+ }
+ postCleanupOnFailure();
}
mController.mLoggerHandler.post(mLogger::logOnSend);
mOverrideOptions = null;
@@ -1143,6 +1154,14 @@
info.releaseAnimSurfaces();
}
+ private void postCleanupOnFailure() {
+ mController.mAtm.mH.post(() -> {
+ synchronized (mController.mAtm.mGlobalLock) {
+ cleanUpOnFailure();
+ }
+ });
+ }
+
/**
* If the remote failed for any reason, use this to do any appropriate clean-up. Do not call
* this directly, it's designed to by called by {@link TransitionController} only.
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 18788bf..2c23b5d 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -126,6 +126,13 @@
final Handler mLoggerHandler = FgThread.getHandler();
+ /**
+ * {@code true} While this waits for the display to become enabled (during boot). While waiting
+ * for the display, all core-initiated transitions will be "local".
+ * Note: This defaults to false so that it doesn't interfere with unit tests.
+ */
+ boolean mIsWaitingForDisplayEnabled = false;
+
TransitionController(ActivityTaskManagerService atm,
TaskSnapshotController taskSnapshotController,
TransitionTracer transitionTracer) {
@@ -486,6 +493,15 @@
Transition requestStartTransition(@NonNull Transition transition, @Nullable Task startTask,
@Nullable RemoteTransition remoteTransition,
@Nullable TransitionRequestInfo.DisplayChange displayChange) {
+ if (mIsWaitingForDisplayEnabled) {
+ ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Disabling player for transition"
+ + " #%d because display isn't enabled yet", transition.getSyncId());
+ transition.mIsPlayerEnabled = false;
+ transition.mLogger.mRequestTimeNs = SystemClock.uptimeNanos();
+ mAtm.mH.post(() -> mAtm.mWindowOrganizerController.startTransition(
+ transition.getToken(), null));
+ return transition;
+ }
try {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
"Requesting StartTransition: %s", transition);
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index b46a720..06a8869 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -310,6 +310,7 @@
import com.android.internal.policy.IKeyguardLockedStateListener;
import com.android.internal.policy.IShortcutService;
import com.android.internal.policy.KeyInterceptionInfo;
+import com.android.internal.protolog.ProtoLogGroup;
import com.android.internal.protolog.ProtoLogImpl;
import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.util.DumpUtils;
@@ -3776,6 +3777,12 @@
// Make sure the last requested orientation has been applied.
updateRotationUnchecked(false, false);
+
+ synchronized (mGlobalLock) {
+ mAtmService.getTransitionController().mIsWaitingForDisplayEnabled = false;
+ ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Notified TransitionController "
+ + "that the display is ready.");
+ }
}
private boolean checkBootAnimationCompleteLocked() {
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 2e9c9cf..73e417e 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -168,6 +168,7 @@
void setWindowManager(WindowManagerService wms) {
mTransitionController = new TransitionController(mService, wms.mTaskSnapshotController,
wms.mTransitionTracer);
+ mTransitionController.mIsWaitingForDisplayEnabled = !wms.mDisplayEnabled;
mTransitionController.registerLegacyListener(wms.mActivityManagerAppTransitionNotifier);
}
diff --git a/services/core/jni/com_android_server_companion_virtual_InputController.cpp b/services/core/jni/com_android_server_companion_virtual_InputController.cpp
index 4cb7a8f..dd757bc 100644
--- a/services/core/jni/com_android_server_companion_virtual_InputController.cpp
+++ b/services/core/jni/com_android_server_companion_virtual_InputController.cpp
@@ -29,8 +29,17 @@
#include <utils/Log.h>
#include <map>
+#include <set>
#include <string>
+/**
+ * Log debug messages about native virtual input devices.
+ * Enable this via "adb shell setprop log.tag.InputController DEBUG"
+ */
+static bool isDebug() {
+ return __android_log_is_loggable(ANDROID_LOG_DEBUG, LOG_TAG, ANDROID_LOG_INFO);
+}
+
namespace android {
enum class DeviceType {
@@ -194,6 +203,15 @@
{AKEYCODE_NUMPAD_COMMA, KEY_KPCOMMA},
};
+/*
+ * Map from the uinput touchscreen fd to the pointers present in the previous touch events that
+ * hasn't been lifted.
+ * We only allow pointer id to go up to MAX_POINTERS because the maximum slots of virtual
+ * touchscreen is set up with MAX_POINTERS. Note that in other cases Android allows pointer id to go
+ * up to MAX_POINTERS_ID.
+ */
+static std::map<int32_t, std::bitset<MAX_POINTERS>> unreleasedTouches;
+
/** Creates a new uinput device and assigns a file descriptor. */
static int openUinput(const char* readableName, jint vendorId, jint productId, const char* phys,
DeviceType deviceType, jint screenHeight, jint screenWidth) {
@@ -366,6 +384,12 @@
static bool nativeCloseUinput(JNIEnv* env, jobject thiz, jint fd) {
ioctl(fd, UI_DEV_DESTROY);
+ if (auto touchesOnFd = unreleasedTouches.find(fd); touchesOnFd != unreleasedTouches.end()) {
+ const size_t remainingPointers = touchesOnFd->second.size();
+ unreleasedTouches.erase(touchesOnFd);
+ ALOGW_IF(remainingPointers > 0, "Closing touchscreen %d, erased %zu unreleased pointers.",
+ fd, remainingPointers);
+ }
return close(fd);
}
@@ -425,6 +449,69 @@
return true;
}
+static bool handleTouchUp(int fd, int pointerId) {
+ if (!writeInputEvent(fd, EV_ABS, ABS_MT_TRACKING_ID, static_cast<int32_t>(-1))) {
+ return false;
+ }
+ auto touchesOnFd = unreleasedTouches.find(fd);
+ if (touchesOnFd == unreleasedTouches.end()) {
+ ALOGE("PointerId %d action UP received with no prior events on touchscreen %d.", pointerId,
+ fd);
+ return false;
+ }
+ ALOGD_IF(isDebug(), "Unreleased touches found for touchscreen %d in the map", fd);
+
+ // When a pointer is no longer in touch, remove the pointer id from the corresponding
+ // entry in the unreleased touches map.
+ if (pointerId < 0 || pointerId >= MAX_POINTERS) {
+ ALOGE("Virtual touch event has invalid pointer id %d; value must be between 0 and %zu",
+ pointerId, MAX_POINTERS - 1);
+ return false;
+ }
+ if (!touchesOnFd->second.test(pointerId)) {
+ ALOGE("PointerId %d action UP received with no prior action DOWN on touchscreen %d.",
+ pointerId, fd);
+ return false;
+ }
+ touchesOnFd->second.reset(pointerId);
+ ALOGD_IF(isDebug(), "Pointer %d erased from the touchscreen %d", pointerId, fd);
+
+ // Only sends the BTN UP event when there's no pointers on the touchscreen.
+ if (touchesOnFd->second.none()) {
+ unreleasedTouches.erase(touchesOnFd);
+ if (!writeInputEvent(fd, EV_KEY, BTN_TOUCH, static_cast<int32_t>(UinputAction::RELEASE))) {
+ return false;
+ }
+ ALOGD_IF(isDebug(), "No pointers on touchscreen %d, BTN UP event sent.", fd);
+ }
+ return true;
+}
+
+static bool handleTouchDown(int fd, int pointerId) {
+ // When a new pointer is down on the touchscreen, add the pointer id in the corresponding
+ // entry in the unreleased touches map.
+ auto touchesOnFd = unreleasedTouches.find(fd);
+ if (touchesOnFd == unreleasedTouches.end()) {
+ // Only sends the BTN Down event when the first pointer on the touchscreen is down.
+ if (!writeInputEvent(fd, EV_KEY, BTN_TOUCH, static_cast<int32_t>(UinputAction::PRESS))) {
+ return false;
+ }
+ touchesOnFd = unreleasedTouches.insert({fd, {}}).first;
+ ALOGD_IF(isDebug(), "New touchscreen with fd %d added in the unreleased touches map.", fd);
+ }
+ if (touchesOnFd->second.test(pointerId)) {
+ ALOGE("Repetitive action DOWN event received on a pointer %d that is already down.",
+ pointerId);
+ return false;
+ }
+ touchesOnFd->second.set(pointerId);
+ ALOGD_IF(isDebug(), "Added pointer %d under touchscreen %d in the map", pointerId, fd);
+ if (!writeInputEvent(fd, EV_ABS, ABS_MT_TRACKING_ID, static_cast<int32_t>(pointerId))) {
+ return false;
+ }
+ return true;
+}
+
static bool nativeWriteTouchEvent(JNIEnv* env, jobject thiz, jint fd, jint pointerId, jint toolType,
jint action, jfloat locationX, jfloat locationY, jfloat pressure,
jfloat majorAxisSize) {
@@ -446,15 +533,10 @@
return false;
}
UinputAction uinputAction = actionIterator->second;
- if (uinputAction == UinputAction::PRESS || uinputAction == UinputAction::RELEASE) {
- if (!writeInputEvent(fd, EV_KEY, BTN_TOUCH, static_cast<int32_t>(uinputAction))) {
- return false;
- }
- if (!writeInputEvent(fd, EV_ABS, ABS_MT_TRACKING_ID,
- static_cast<int32_t>(uinputAction == UinputAction::PRESS ? pointerId
- : -1))) {
- return false;
- }
+ if (uinputAction == UinputAction::PRESS && !handleTouchDown(fd, pointerId)) {
+ return false;
+ } else if (uinputAction == UinputAction::RELEASE && !handleTouchUp(fd, pointerId)) {
+ return false;
}
if (!writeInputEvent(fd, EV_ABS, ABS_MT_POSITION_X, locationX)) {
return false;
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index d22be9e..5c6cc9f 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -38,6 +38,7 @@
import android.app.SystemServiceRegistry;
import android.app.admin.DevicePolicySafetyChecker;
import android.app.usage.UsageStatsManagerInternal;
+import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
@@ -2894,6 +2895,27 @@
t.traceEnd();
}
+ if (isWatch) {
+ t.traceBegin("StartWearService");
+ String wearServiceComponentNameString =
+ context.getString(R.string.config_wearServiceComponent);
+
+ if (!TextUtils.isEmpty(wearServiceComponentNameString)) {
+ ComponentName wearServiceComponentName = ComponentName.unflattenFromString(
+ wearServiceComponentNameString);
+
+ if (wearServiceComponentName != null) {
+ Intent intent = new Intent();
+ intent.setComponent(wearServiceComponentName);
+ intent.addFlags(Intent.FLAG_DIRECT_BOOT_AUTO);
+ context.startServiceAsUser(intent, UserHandle.SYSTEM);
+ } else {
+ Slog.d(TAG, "Null wear service component name.");
+ }
+ }
+ t.traceEnd();
+ }
+
// Enable airplane mode in safe mode. setAirplaneMode() cannot be called
// earlier as it sends broadcasts to other services.
// TODO: This may actually be too late if radio firmware already started leaking
diff --git a/services/permission/java/com/android/server/permission/access/permission/UidPermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/UidPermissionPolicy.kt
index 02da25d..59551a3 100644
--- a/services/permission/java/com/android/server/permission/access/permission/UidPermissionPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/UidPermissionPolicy.kt
@@ -196,7 +196,9 @@
val changedPermissionNames = IndexedSet<String>()
trimPermissions(packageName, changedPermissionNames)
- trimPermissionStates(appId)
+ if (appId in newState.systemState.appIds) {
+ trimPermissionStates(appId)
+ }
changedPermissionNames.forEachIndexed { _, permissionName ->
evaluatePermissionStateForAllPackages(permissionName, null)
}
@@ -1001,10 +1003,7 @@
permissionName: String
): Boolean? {
val permissionAllowlist = newState.systemState.permissionAllowlist
- val apexModuleName = permissionAllowlist.apexPrivilegedAppAllowlists
- .firstNotNullOfOrNullIndexed { _, apexModuleName, apexAllowlist ->
- if (packageState.apexModuleName in apexAllowlist) apexModuleName else null
- }
+ val apexModuleName = packageState.apexModuleName
val packageName = packageState.packageName
return when {
packageState.isVendor -> permissionAllowlist.getVendorPrivilegedAppAllowlistState(
@@ -1071,7 +1070,7 @@
state: AccessState = newState,
action: (PackageState) -> Unit
) {
- val packageNames = state.systemState.appIds[appId]
+ val packageNames = state.systemState.appIds[appId]!!
packageNames.forEachIndexed { _, packageName ->
val packageState = state.systemState.packageStates[packageName]!!
if (packageState.androidPackage != null) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
index 9f7b72c..fb05699 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
@@ -17,7 +17,7 @@
package com.android.server.am;
import static android.app.ActivityManager.PROCESS_CAPABILITY_ALL;
-import static android.app.ActivityManager.PROCESS_CAPABILITY_NETWORK;
+import static android.app.ActivityManager.PROCESS_CAPABILITY_BFSL;
import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
import static android.app.ActivityManager.PROCESS_STATE_BOUND_TOP;
import static android.app.ActivityManager.PROCESS_STATE_CACHED_ACTIVITY;
@@ -38,6 +38,7 @@
import static android.app.ActivityManager.PROCESS_STATE_TOP;
import static android.app.ActivityManager.PROCESS_STATE_TOP_SLEEPING;
import static android.app.ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
@@ -226,6 +227,15 @@
}
}
+ private static void assertBfsl(ProcessRecord app) {
+ assertEquals(PROCESS_CAPABILITY_BFSL,
+ app.mState.getSetCapability() & PROCESS_CAPABILITY_BFSL);
+ }
+
+ private static void assertNoBfsl(ProcessRecord app) {
+ assertEquals(0, app.mState.getSetCapability() & PROCESS_CAPABILITY_BFSL);
+ }
+
/**
* Replace the process LRU with the given processes.
* @param apps
@@ -264,6 +274,7 @@
assertProcStates(app, PROCESS_STATE_BOUND_FOREGROUND_SERVICE, PERSISTENT_PROC_ADJ,
SCHED_GROUP_RESTRICTED);
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -335,6 +346,7 @@
assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, FOREGROUND_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -447,6 +459,7 @@
assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -460,6 +473,7 @@
assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -471,7 +485,7 @@
ServiceRecord s = ServiceRecord.newEmptyInstanceForTest(sService);
s.startRequested = true;
s.isForeground = true;
- s.foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE;
+ s.foregroundServiceType = FOREGROUND_SERVICE_TYPE_SHORT_SERVICE;
s.setShortFgsInfo(SystemClock.uptimeMillis());
// SHORT_SERVICE FGS will get IMP_FG and a slightly different recent-adjustment.
@@ -480,16 +494,15 @@
MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
app.mServices.startService(s);
app.mServices.setHasForegroundServices(true,
- ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE, /* hasNoneType=*/false);
+ FOREGROUND_SERVICE_TYPE_SHORT_SERVICE, /* hasNoneType=*/false);
app.mState.setLastTopTime(SystemClock.uptimeMillis());
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
updateOomAdj(app);
- assertProcStates(app, PROCESS_STATE_IMPORTANT_FOREGROUND,
+ assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE,
PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ + 1, SCHED_GROUP_DEFAULT);
- // Should get network access.
- assertTrue((app.mState.getSetCapability() & PROCESS_CAPABILITY_NETWORK) != 0);
+ assertNoBfsl(app);
}
// SHORT_SERVICE, but no longer recent.
@@ -497,7 +510,7 @@
ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
app.mServices.setHasForegroundServices(true,
- ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE, /* hasNoneType=*/false);
+ FOREGROUND_SERVICE_TYPE_SHORT_SERVICE, /* hasNoneType=*/false);
app.mServices.startService(s);
app.mState.setLastTopTime(SystemClock.uptimeMillis()
- sService.mConstants.TOP_TO_FGS_GRACE_DURATION);
@@ -505,17 +518,16 @@
updateOomAdj(app);
- assertProcStates(app, PROCESS_STATE_IMPORTANT_FOREGROUND,
+ assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE,
PERCEPTIBLE_MEDIUM_APP_ADJ + 1, SCHED_GROUP_DEFAULT);
- // Still should get network access.
- assertTrue((app.mState.getSetCapability() & PROCESS_CAPABILITY_NETWORK) != 0);
+ assertNoBfsl(app);
}
// SHORT_SERVICE, timed out already.
s = ServiceRecord.newEmptyInstanceForTest(sService);
s.startRequested = true;
s.isForeground = true;
- s.foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE;
+ s.foregroundServiceType = FOREGROUND_SERVICE_TYPE_SHORT_SERVICE;
s.setShortFgsInfo(SystemClock.uptimeMillis()
- sService.mConstants.mShortFgsTimeoutDuration
- sService.mConstants.mShortFgsProcStateExtraWaitDuration);
@@ -523,7 +535,7 @@
ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
app.mServices.setHasForegroundServices(true,
- ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE, /* hasNoneType=*/false);
+ FOREGROUND_SERVICE_TYPE_SHORT_SERVICE, /* hasNoneType=*/false);
app.mServices.startService(s);
app.mState.setLastTopTime(SystemClock.uptimeMillis()
- sService.mConstants.TOP_TO_FGS_GRACE_DURATION);
@@ -533,9 +545,7 @@
// Procstate should be lower than FGS. (It should be SERVICE)
assertEquals(app.mState.getSetProcState(), PROCESS_STATE_SERVICE);
-
- // Shouldn't have the network capability now.
- assertTrue((app.mState.getSetCapability() & PROCESS_CAPABILITY_NETWORK) == 0);
+ assertNoBfsl(app);
}
}
@@ -564,6 +574,7 @@
assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE,
PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ, SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -925,6 +936,7 @@
assertProcStates(app, PROCESS_STATE_BOUND_FOREGROUND_SERVICE, VISIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -940,6 +952,7 @@
updateOomAdj(client, app);
assertEquals(FOREGROUND_APP_ADJ, app.mState.getSetAdj());
+ assertNoBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -973,6 +986,8 @@
assertEquals(PROCESS_STATE_BOUND_FOREGROUND_SERVICE, app.mState.getSetProcState());
assertEquals(PROCESS_STATE_PERSISTENT, client.mState.getSetProcState());
+ assertBfsl(client);
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -988,6 +1003,7 @@
updateOomAdj(app);
assertEquals(PROCESS_STATE_TRANSIENT_BACKGROUND, app.mState.getSetProcState());
+ assertNoBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -1002,7 +1018,92 @@
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
updateOomAdj(client, app);
+ assertEquals(PROCESS_STATE_FOREGROUND_SERVICE, client.mState.getSetProcState());
+ assertBfsl(client);
assertEquals(PROCESS_STATE_FOREGROUND_SERVICE, app.mState.getSetProcState());
+ assertBfsl(app);
+ }
+
+ @SuppressWarnings("GuardedBy")
+ @Test
+ public void testUpdateOomAdj_DoOne_Service_ImportantFgService_ShortFgs() {
+ // Client has a SHORT_SERVICE FGS, which isn't allowed BFSL.
+ ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
+ MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, false));
+ ProcessRecord client = spy(makeDefaultProcessRecord(MOCKAPP2_PID, MOCKAPP2_UID,
+ MOCKAPP2_PROCESSNAME, MOCKAPP2_PACKAGENAME, false));
+ bindService(app, client, null, 0, mock(IBinder.class));
+
+ // In order to trick OomAdjuster to think it has a short-service, we need this logic.
+ ServiceRecord s = ServiceRecord.newEmptyInstanceForTest(sService);
+ s.startRequested = true;
+ s.isForeground = true;
+ s.foregroundServiceType = FOREGROUND_SERVICE_TYPE_SHORT_SERVICE;
+ s.setShortFgsInfo(SystemClock.uptimeMillis());
+ client.mServices.startService(s);
+ client.mState.setLastTopTime(SystemClock.uptimeMillis());
+
+ client.mServices.setHasForegroundServices(true, FOREGROUND_SERVICE_TYPE_SHORT_SERVICE,
+ /* hasNoneType=*/false);
+ sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
+ updateOomAdj(client, app);
+
+ // Client only has a SHORT_FGS, so it doesn't have BFSL, and that's propagated.
+ assertEquals(PROCESS_STATE_FOREGROUND_SERVICE, client.mState.getSetProcState());
+ assertNoBfsl(client);
+ assertEquals(PROCESS_STATE_FOREGROUND_SERVICE, app.mState.getSetProcState());
+ assertNoBfsl(app);
+ }
+
+ @SuppressWarnings("GuardedBy")
+ @Test
+ public void testUpdateOomAdj_DoOne_Service_BoundForegroundService_with_ShortFgs() {
+
+ // app2, which is bound by app1 (which makes it BFGS)
+ // but it also has a short-fgs.
+ ProcessRecord app2 = spy(makeDefaultProcessRecord(MOCKAPP3_PID, MOCKAPP3_UID,
+ MOCKAPP3_PROCESSNAME, MOCKAPP3_PACKAGENAME, false));
+
+ // In order to trick OomAdjuster to think it has a short-service, we need this logic.
+ ServiceRecord s = ServiceRecord.newEmptyInstanceForTest(sService);
+ s.startRequested = true;
+ s.isForeground = true;
+ s.foregroundServiceType = FOREGROUND_SERVICE_TYPE_SHORT_SERVICE;
+ s.setShortFgsInfo(SystemClock.uptimeMillis());
+ app2.mServices.startService(s);
+ app2.mState.setLastTopTime(SystemClock.uptimeMillis());
+
+ app2.mServices.setHasForegroundServices(true, FOREGROUND_SERVICE_TYPE_SHORT_SERVICE,
+ /* hasNoneType=*/false);
+ sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
+ updateOomAdj(app2);
+
+ // Client only has a SHORT_FGS, so it doesn't have BFSL, and that's propagated.
+ assertEquals(PROCESS_STATE_FOREGROUND_SERVICE, app2.mState.getSetProcState());
+ assertNoBfsl(app2);
+
+ // Now, create a BFGS process (app1), and make it bind to app 2
+
+ // Persistent process
+ ProcessRecord pers = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
+ MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, false));
+ pers.mState.setMaxAdj(PERSISTENT_PROC_ADJ);
+
+ // app1, which is bound by pers (which makes it BFGS)
+ ProcessRecord app1 = spy(makeDefaultProcessRecord(MOCKAPP2_PID, MOCKAPP2_UID,
+ MOCKAPP2_PROCESSNAME, MOCKAPP2_PACKAGENAME, false));
+
+ bindService(app1, pers, null, Context.BIND_FOREGROUND_SERVICE, mock(IBinder.class));
+ bindService(app2, app1, null, 0, mock(IBinder.class));
+
+ updateOomAdj(pers, app1, app2);
+
+ assertEquals(PROCESS_STATE_BOUND_FOREGROUND_SERVICE, app1.mState.getSetProcState());
+ assertBfsl(app1);
+
+ // Now, app2 gets BFSL from app1.
+ assertEquals(PROCESS_STATE_FOREGROUND_SERVICE, app2.mState.getSetProcState());
+ assertBfsl(app2);
}
@SuppressWarnings("GuardedBy")
@@ -1022,11 +1123,13 @@
doReturn(null).when(sService.mBackupTargets).get(anyInt());
assertEquals(BACKUP_APP_ADJ, app.mState.getSetAdj());
+ assertNoBfsl(app);
client.mState.setMaxAdj(PERSISTENT_PROC_ADJ);
updateOomAdj(app);
assertEquals(PERSISTENT_SERVICE_ADJ, app.mState.getSetAdj());
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -1172,6 +1275,7 @@
updateOomAdj(client, app);
assertEquals(PROCESS_STATE_IMPORTANT_BACKGROUND, app.mState.getSetProcState());
+ assertNoBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -1231,6 +1335,42 @@
assertProcStates(app, PROCESS_STATE_BOUND_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
+ }
+
+ @SuppressWarnings("GuardedBy")
+ @Test
+ public void testUpdateOomAdj_DoOne_Provider_FgService_ShortFgs() {
+ // Client has a SHORT_SERVICE FGS, which isn't allowed BFSL.
+ ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
+ MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, false));
+ ProcessRecord client = spy(makeDefaultProcessRecord(MOCKAPP2_PID, MOCKAPP2_UID,
+ MOCKAPP2_PROCESSNAME, MOCKAPP2_PACKAGENAME, false));
+
+ // In order to trick OomAdjuster to think it has a short-service, we need this logic.
+ ServiceRecord s = ServiceRecord.newEmptyInstanceForTest(sService);
+ s.startRequested = true;
+ s.isForeground = true;
+ s.foregroundServiceType = FOREGROUND_SERVICE_TYPE_SHORT_SERVICE;
+ s.setShortFgsInfo(SystemClock.uptimeMillis());
+ client.mServices.startService(s);
+ client.mState.setLastTopTime(SystemClock.uptimeMillis());
+
+ client.mServices.setHasForegroundServices(true, FOREGROUND_SERVICE_TYPE_SHORT_SERVICE,
+ /* hasNoneType=*/false);
+ bindProvider(app, client, null, null, false);
+ sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
+ updateOomAdj(client, app);
+
+ // Client only has a SHORT_FGS, so it doesn't have BFSL, and that's propagated.
+ assertProcStates(app, PROCESS_STATE_BOUND_FOREGROUND_SERVICE,
+ PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ + 1,
+ SCHED_GROUP_DEFAULT);
+ assertNoBfsl(client);
+ assertProcStates(app, PROCESS_STATE_BOUND_FOREGROUND_SERVICE,
+ PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ + 1,
+ SCHED_GROUP_DEFAULT);
+ assertNoBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -1299,6 +1439,7 @@
assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -1318,6 +1459,7 @@
assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -1346,6 +1488,9 @@
SCHED_GROUP_DEFAULT);
assertProcStates(client2, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
+ assertBfsl(client);
+ assertBfsl(client2);
client2.mServices.setHasForegroundServices(false, 0, /* hasNoneType=*/false);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
@@ -1354,6 +1499,9 @@
assertEquals(PROCESS_STATE_CACHED_EMPTY, client2.mState.getSetProcState());
assertEquals(PROCESS_STATE_CACHED_EMPTY, client.mState.getSetProcState());
assertEquals(PROCESS_STATE_CACHED_EMPTY, app.mState.getSetProcState());
+ assertNoBfsl(app);
+ assertNoBfsl(client);
+ assertNoBfsl(client2);
}
@SuppressWarnings("GuardedBy")
@@ -1378,6 +1526,9 @@
SCHED_GROUP_DEFAULT);
assertProcStates(client2, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
+ assertBfsl(client);
+ assertBfsl(client2);
}
@SuppressWarnings("GuardedBy")
@@ -1402,6 +1553,9 @@
SCHED_GROUP_DEFAULT);
assertProcStates(client2, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
+ assertBfsl(client);
+ assertBfsl(client2);
}
@SuppressWarnings("GuardedBy")
@@ -1437,6 +1591,11 @@
SCHED_GROUP_DEFAULT);
assertProcStates(client4, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
+ assertBfsl(client);
+ assertBfsl(client2);
+ assertBfsl(client3);
+ assertBfsl(client4);
}
@SuppressWarnings("GuardedBy")
@@ -1461,6 +1620,7 @@
assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -1542,6 +1702,7 @@
assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -1567,6 +1728,7 @@
assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -1586,6 +1748,7 @@
assertProcStates(app, PROCESS_STATE_BOUND_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -1606,6 +1769,7 @@
assertProcStates(app, PROCESS_STATE_BOUND_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -1625,6 +1789,7 @@
assertProcStates(app, PROCESS_STATE_BOUND_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -1645,6 +1810,7 @@
assertProcStates(app, PROCESS_STATE_BOUND_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -1672,6 +1838,8 @@
SCHED_GROUP_DEFAULT);
assertProcStates(app2, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app1);
+ assertBfsl(app2);
bindService(app1, client1, null, Context.BIND_SCHEDULE_LIKE_TOP_APP, mock(IBinder.class));
bindService(app2, client2, null, Context.BIND_SCHEDULE_LIKE_TOP_APP, mock(IBinder.class));
@@ -1688,6 +1856,7 @@
SCHED_GROUP_TOP_APP);
assertProcStates(app2, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app2);
}
@SuppressWarnings("GuardedBy")
@@ -1770,6 +1939,7 @@
assertProcStates(app1, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app1);
}
@SuppressWarnings("GuardedBy")
@@ -1790,6 +1960,7 @@
assertProcStates(app1, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app1);
}
@SuppressWarnings("GuardedBy")
@@ -1912,6 +2083,7 @@
SCHED_GROUP_DEFAULT);
assertProcStates(app2, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app2);
}
@SuppressWarnings("GuardedBy")
@@ -1931,6 +2103,8 @@
SCHED_GROUP_DEFAULT);
assertProcStates(app2, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
+ assertBfsl(app2);
}
@SuppressWarnings("GuardedBy")
@@ -1964,6 +2138,9 @@
assertEquals(false, app.mState.isEmpty());
assertEquals(false, app2.mState.isEmpty());
assertEquals(false, app3.mState.isEmpty());
+ assertBfsl(app);
+ assertBfsl(app2);
+ assertBfsl(app3);
}
@SuppressWarnings("GuardedBy")
@@ -2001,6 +2178,11 @@
SCHED_GROUP_DEFAULT);
assertProcStates(app5, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
+ assertBfsl(app2);
+ assertBfsl(app3);
+ // 4 is IMP_FG
+ assertBfsl(app5);
}
@SuppressWarnings("GuardedBy")
@@ -2038,6 +2220,11 @@
SCHED_GROUP_DEFAULT);
assertProcStates(app5, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
+ assertBfsl(app2);
+ assertBfsl(app3);
+ // 4 is IMP_FG
+ assertBfsl(app5);
}
@SuppressWarnings("GuardedBy")
@@ -2075,6 +2262,11 @@
SCHED_GROUP_DEFAULT);
assertProcStates(app5, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
+ assertBfsl(app2);
+ assertBfsl(app3);
+ // 4 is IMP_FG
+ assertBfsl(app5);
}
@SuppressWarnings("GuardedBy")
@@ -2096,7 +2288,7 @@
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
updateOomAdj(app, client, client2, client3);
- final int expected = PROCESS_CAPABILITY_ALL;
+ final int expected = PROCESS_CAPABILITY_ALL & ~PROCESS_CAPABILITY_BFSL;
assertEquals(expected, client.mState.getSetCapability());
assertEquals(expected, client2.mState.getSetCapability());
assertEquals(expected, app.mState.getSetCapability());
@@ -2137,6 +2329,11 @@
SCHED_GROUP_DEFAULT);
assertProcStates(app5, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
+ assertBfsl(app2);
+ assertBfsl(app3);
+ // 4 is IMP_FG
+ assertBfsl(app5);
}
@SuppressWarnings("GuardedBy")
@@ -2444,6 +2641,15 @@
assertEquals(expectedProcState, state.getSetProcState());
assertEquals(expectedAdj, state.getSetAdj());
assertEquals(expectedSchedGroup, state.getSetSchedGroup());
+
+ // Below BFGS should never have BFSL.
+ if (expectedProcState > PROCESS_STATE_BOUND_FOREGROUND_SERVICE) {
+ assertNoBfsl(app);
+ }
+ // Above FGS should always have BFSL.
+ if (expectedProcState < PROCESS_STATE_FOREGROUND_SERVICE) {
+ assertBfsl(app);
+ }
}
private void assertProcStates(ProcessRecord app, boolean expectedCached,
@@ -2453,5 +2659,14 @@
assertEquals(expectedProcState, state.getSetProcState());
assertEquals(expectedAdj, state.getSetAdj());
assertEquals(expectedAdjType, state.getAdjType());
+
+ // Below BFGS should never have BFSL.
+ if (expectedProcState > PROCESS_STATE_BOUND_FOREGROUND_SERVICE) {
+ assertNoBfsl(app);
+ }
+ // Above FGS should always have BFSL.
+ if (expectedProcState < PROCESS_STATE_FOREGROUND_SERVICE) {
+ assertBfsl(app);
+ }
}
}
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
index 27d912b..0eff0da 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
@@ -2402,6 +2402,74 @@
eq(lightSensorTwo), anyInt(), any(Handler.class));
}
+ @Test
+ public void testAuthenticationPossibleSetsPhysicalRateRangesToMax() throws RemoteException {
+ DisplayModeDirector director =
+ createDirectorFromRefreshRateArray(new float[]{60.0f, 90.0f}, 0);
+ // don't call director.start(createMockSensorManager());
+ // DisplayObserver will reset mSupportedModesByDisplay
+ director.onBootCompleted();
+ ArgumentCaptor<IUdfpsRefreshRateRequestCallback> captor =
+ ArgumentCaptor.forClass(IUdfpsRefreshRateRequestCallback.class);
+ verify(mStatusBarMock).setUdfpsRefreshRateCallback(captor.capture());
+
+ captor.getValue().onAuthenticationPossible(DISPLAY_ID, true);
+
+ Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE);
+ assertThat(vote.refreshRateRanges.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
+ assertThat(vote.refreshRateRanges.physical.max).isWithin(FLOAT_TOLERANCE).of(90);
+ }
+
+ @Test
+ public void testAuthenticationPossibleUnsetsVote() throws RemoteException {
+ DisplayModeDirector director =
+ createDirectorFromRefreshRateArray(new float[]{60.0f, 90.0f}, 0);
+ director.start(createMockSensorManager());
+ director.onBootCompleted();
+ ArgumentCaptor<IUdfpsRefreshRateRequestCallback> captor =
+ ArgumentCaptor.forClass(IUdfpsRefreshRateRequestCallback.class);
+ verify(mStatusBarMock).setUdfpsRefreshRateCallback(captor.capture());
+ captor.getValue().onAuthenticationPossible(DISPLAY_ID, true);
+ captor.getValue().onAuthenticationPossible(DISPLAY_ID, false);
+
+ Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE);
+ assertNull(vote);
+ }
+
+ @Test
+ public void testUdfpsRequestSetsPhysicalRateRangesToMax() throws RemoteException {
+ DisplayModeDirector director =
+ createDirectorFromRefreshRateArray(new float[]{60.0f, 90.0f}, 0);
+ // don't call director.start(createMockSensorManager());
+ // DisplayObserver will reset mSupportedModesByDisplay
+ director.onBootCompleted();
+ ArgumentCaptor<IUdfpsRefreshRateRequestCallback> captor =
+ ArgumentCaptor.forClass(IUdfpsRefreshRateRequestCallback.class);
+ verify(mStatusBarMock).setUdfpsRefreshRateCallback(captor.capture());
+
+ captor.getValue().onRequestEnabled(DISPLAY_ID);
+
+ Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_UDFPS);
+ assertThat(vote.refreshRateRanges.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
+ assertThat(vote.refreshRateRanges.physical.max).isWithin(FLOAT_TOLERANCE).of(90);
+ }
+
+ @Test
+ public void testUdfpsRequestUnsetsUnsetsVote() throws RemoteException {
+ DisplayModeDirector director =
+ createDirectorFromRefreshRateArray(new float[]{60.0f, 90.0f}, 0);
+ director.start(createMockSensorManager());
+ director.onBootCompleted();
+ ArgumentCaptor<IUdfpsRefreshRateRequestCallback> captor =
+ ArgumentCaptor.forClass(IUdfpsRefreshRateRequestCallback.class);
+ verify(mStatusBarMock).setUdfpsRefreshRateCallback(captor.capture());
+ captor.getValue().onRequestEnabled(DISPLAY_ID);
+ captor.getValue().onRequestDisabled(DISPLAY_ID);
+
+ Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_UDFPS);
+ assertNull(vote);
+ }
+
private Temperature getSkinTemp(@Temperature.ThrottlingStatus int status) {
return new Temperature(30.0f, Temperature.TYPE_SKIN, "test_skin_temp", status);
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
index 42bbd2d..94193f4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
@@ -352,6 +352,7 @@
mAtmService.setWindowManager(mWmService);
mWmService.mDisplayEnabled = true;
mWmService.mDisplayReady = true;
+ mAtmService.getTransitionController().mIsWaitingForDisplayEnabled = false;
// Set configuration for default display
mWmService.getDefaultDisplayContentLocked().reconfigureDisplayLocked();
diff --git a/telecomm/java/android/telecom/CallStreamingService.java b/telecomm/java/android/telecom/CallStreamingService.java
index affa6b6..7f11128 100644
--- a/telecomm/java/android/telecom/CallStreamingService.java
+++ b/telecomm/java/android/telecom/CallStreamingService.java
@@ -79,7 +79,7 @@
break;
case MSG_CALL_STREAMING_CHANGED_CHANGED:
int state = (int) msg.obj;
- mCall.setStreamingState(state);
+ mCall.requestStreamingState(state);
CallStreamingService.this.onCallStreamingStateChanged(state);
break;
default:
diff --git a/telecomm/java/android/telecom/StreamingCall.java b/telecomm/java/android/telecom/StreamingCall.java
index 985cccc..4b27dd6 100644
--- a/telecomm/java/android/telecom/StreamingCall.java
+++ b/telecomm/java/android/telecom/StreamingCall.java
@@ -172,7 +172,7 @@
* to request holding, unholding and disconnecting this {@code StreamingCall}.
* @param state The current streaming state of the call.
*/
- public void setStreamingState(@StreamingCallState int state) {
+ public void requestStreamingState(@StreamingCallState int state) {
mAdapter.setStreamingState(state);
}
}
diff --git a/telecomm/java/android/telecom/StreamingCallAdapter.java b/telecomm/java/android/telecom/StreamingCallAdapter.java
index bd8727d..54a3e24 100644
--- a/telecomm/java/android/telecom/StreamingCallAdapter.java
+++ b/telecomm/java/android/telecom/StreamingCallAdapter.java
@@ -25,7 +25,7 @@
* Telecom. When Telecom binds to a {@link CallStreamingService}, an instance of this class is given
* to the general streaming app through which it can manipulate the streaming calls. Whe the general
* streaming app is notified of new ongoing streaming calls, it can execute
- * {@link StreamingCall#setStreamingState(int)} for the ongoing streaming calls the user on the
+ * {@link StreamingCall#requestStreamingState(int)} for the ongoing streaming calls the user on the
* receiver side would like to hold, unhold and disconnect.
*
* @hide
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index c238f24..819b0b2 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -14894,7 +14894,10 @@
/**
* The extra used with an {@link #ACTION_NETWORK_COUNTRY_CHANGED} to specify the
- * the country code in ISO-3166-1 alpha-2 format.
+ * the country code in ISO-3166-1 alpha-2 format. This is the same country code returned by
+ * {@link #getNetworkCountryIso()}. This might be an empty string when the country code is not
+ * available.
+ *
* <p class="note">
* Retrieve with {@link android.content.Intent#getStringExtra(String)}.
*/
@@ -14903,11 +14906,11 @@
/**
* The extra used with an {@link #ACTION_NETWORK_COUNTRY_CHANGED} to specify the
- * last known the country code in ISO-3166-1 alpha-2 format.
+ * last known the country code in ISO-3166-1 alpha-2 format. This might be an empty string when
+ * the country code was never available. The last known country code persists across reboot.
+ *
* <p class="note">
* Retrieve with {@link android.content.Intent#getStringExtra(String)}.
- *
- * @hide
*/
public static final String EXTRA_LAST_KNOWN_NETWORK_COUNTRY =
"android.telephony.extra.LAST_KNOWN_NETWORK_COUNTRY";
diff --git a/wifi/java/Android.bp b/wifi/java/Android.bp
index 225e750..434226d 100644
--- a/wifi/java/Android.bp
+++ b/wifi/java/Android.bp
@@ -27,7 +27,10 @@
filegroup {
name: "framework-wifi-non-updatable-sources-internal",
- srcs: ["src/**/*.java"],
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.aidl",
+ ],
path: "src",
visibility: ["//visibility:private"],
}
diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/app/DeviceInfo.aidl b/wifi/java/src/android/net/wifi/sharedconnectivity/app/DeviceInfo.aidl
new file mode 100644
index 0000000..35d5c15
--- /dev/null
+++ b/wifi/java/src/android/net/wifi/sharedconnectivity/app/DeviceInfo.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.app;
+
+parcelable DeviceInfo;
\ No newline at end of file
diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/app/DeviceInfo.java b/wifi/java/src/android/net/wifi/sharedconnectivity/app/DeviceInfo.java
new file mode 100644
index 0000000..7874b2a
--- /dev/null
+++ b/wifi/java/src/android/net/wifi/sharedconnectivity/app/DeviceInfo.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.app;
+
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.net.wifi.sharedconnectivity.service.SharedConnectivityService;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * A data class representing a device providing connectivity.
+ * This class is used in IPC calls between the implementer of {@link SharedConnectivityService} and
+ * the consumers of {@link com.android.wifitrackerlib}.
+ *
+ * @hide
+ */
+@SystemApi
+public final class DeviceInfo implements Parcelable {
+
+ /**
+ * Device type providing connectivity is unknown.
+ */
+ public static final int DEVICE_TYPE_UNKNOWN = 0;
+
+ /**
+ * Device providing connectivity is a mobile phone.
+ */
+ public static final int DEVICE_TYPE_PHONE = 1;
+
+ /**
+ * Device providing connectivity is a tablet.
+ */
+ public static final int DEVICE_TYPE_TABLET = 2;
+
+ /**
+ * Device providing connectivity is a laptop.
+ */
+ public static final int DEVICE_TYPE_LAPTOP = 3;
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ DEVICE_TYPE_UNKNOWN,
+ DEVICE_TYPE_PHONE,
+ DEVICE_TYPE_TABLET,
+ DEVICE_TYPE_LAPTOP
+ })
+ public @interface DeviceType {}
+
+ @DeviceType private final int mDeviceType;
+ private final String mDeviceName;
+ private final String mModelName;
+ private final int mBatteryPercentage;
+ private final int mConnectionStrength;
+
+ /**
+ * Builder class for {@link DeviceInfo}.
+ */
+ public static final class Builder {
+ private int mDeviceType;
+ private String mDeviceName;
+ private String mModelName;
+ private int mBatteryPercentage;
+ private int mConnectionStrength;
+
+ public Builder() {}
+
+ /**
+ * Sets the device type that provides connectivity.
+ *
+ * @param deviceType Device type as represented by IntDef {@link DeviceType}.
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setDeviceType(@DeviceType int deviceType) {
+ mDeviceType = deviceType;
+ return this;
+ }
+
+ /**
+ * Sets the device name of the remote device.
+ *
+ * @param deviceName The user configurable device name.
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setDeviceName(@NonNull String deviceName) {
+ mDeviceName = deviceName;
+ return this;
+ }
+
+ /**
+ * Sets the model name of the remote device.
+ *
+ * @param modelName The OEM configured name for the device model.
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setModelName(@NonNull String modelName) {
+ mModelName = modelName;
+ return this;
+ }
+
+ /**
+ * Sets the battery charge percentage of the remote device.
+ *
+ * @param batteryPercentage The battery charge percentage in the range 0 to 100.
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setBatteryPercentage(@IntRange(from = 0, to = 100) int batteryPercentage) {
+ mBatteryPercentage = batteryPercentage;
+ return this;
+ }
+
+ /**
+ * Sets the displayed connection strength of the remote device to the internet.
+ *
+ * @param connectionStrength Connection strength in range 0 to 3.
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setConnectionStrength(@IntRange(from = 0, to = 3) int connectionStrength) {
+ mConnectionStrength = connectionStrength;
+ return this;
+ }
+
+ /**
+ * Builds the {@link DeviceInfo} object.
+ *
+ * @return Returns the built {@link DeviceInfo} object.
+ */
+ @NonNull
+ public DeviceInfo build() {
+ return new DeviceInfo(mDeviceType, mDeviceName, mModelName, mBatteryPercentage,
+ mConnectionStrength);
+ }
+ }
+
+ private static void validate(int deviceType, String deviceName, String modelName,
+ int batteryPercentage, int connectionStrength) {
+ if (deviceType != DEVICE_TYPE_UNKNOWN && deviceType != DEVICE_TYPE_PHONE
+ && deviceType != DEVICE_TYPE_TABLET && deviceType != DEVICE_TYPE_LAPTOP) {
+ throw new IllegalArgumentException("Illegal device type");
+ }
+ if (Objects.isNull(deviceName)) {
+ throw new IllegalArgumentException("DeviceName must be set");
+ }
+ if (Objects.isNull(modelName)) {
+ throw new IllegalArgumentException("ModelName must be set");
+ }
+ if (batteryPercentage < 0 || batteryPercentage > 100) {
+ throw new IllegalArgumentException("BatteryPercentage must be in range 0-100");
+ }
+ if (connectionStrength < 0 || connectionStrength > 3) {
+ throw new IllegalArgumentException("ConnectionStrength must be in range 0-3");
+ }
+ }
+
+ private DeviceInfo(@DeviceType int deviceType, @NonNull String deviceName,
+ @NonNull String modelName, int batteryPercentage, int connectionStrength) {
+ validate(deviceType, deviceName, modelName, batteryPercentage, connectionStrength);
+ mDeviceType = deviceType;
+ mDeviceName = deviceName;
+ mModelName = modelName;
+ mBatteryPercentage = batteryPercentage;
+ mConnectionStrength = connectionStrength;
+ }
+
+ /**
+ * Gets the device type that provides connectivity.
+ *
+ * @return Returns the device type as represented by IntDef {@link DeviceType}.
+ */
+ @DeviceType
+ public int getDeviceType() {
+ return mDeviceType;
+ }
+
+ /**
+ * Gets the device name of the remote device.
+ *
+ * @return Returns the user configurable device name.
+ */
+ @NonNull
+ public String getDeviceName() {
+ return mDeviceName;
+ }
+
+ /**
+ * Gets the model name of the remote device.
+ *
+ * @return Returns the OEM configured name for the device model.
+ */
+ @NonNull
+ public String getModelName() {
+ return mModelName;
+ }
+
+ /**
+ * Gets the battery charge percentage of the remote device.
+ *
+ * @return Returns the battery charge percentage in the range 0 to 100.
+ */
+ @NonNull
+ @IntRange(from = 0, to = 100)
+ public int getBatteryPercentage() {
+ return mBatteryPercentage;
+ }
+
+ /**
+ * Gets the displayed connection strength of the remote device to the internet.
+ *
+ * @return Returns the connection strength in range 0 to 3.
+ */
+ @NonNull
+ @IntRange(from = 0, to = 3)
+ public int getConnectionStrength() {
+ return mConnectionStrength;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof DeviceInfo)) return false;
+ DeviceInfo other = (DeviceInfo) obj;
+ return mDeviceType == other.getDeviceType()
+ && Objects.equals(mDeviceName, other.mDeviceName)
+ && Objects.equals(mModelName, other.mModelName)
+ && mBatteryPercentage == other.mBatteryPercentage
+ && mConnectionStrength == other.mConnectionStrength;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mDeviceType, mDeviceName, mModelName, mBatteryPercentage,
+ mConnectionStrength);
+ }
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mDeviceType);
+ dest.writeString(mDeviceName);
+ dest.writeString(mModelName);
+ dest.writeInt(mBatteryPercentage);
+ dest.writeInt(mConnectionStrength);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @NonNull
+ public static final Creator<DeviceInfo> CREATOR = new Creator<DeviceInfo>() {
+ @Override
+ public DeviceInfo createFromParcel(Parcel in) {
+ return new DeviceInfo(in.readInt(), in.readString(), in.readString(), in.readInt(),
+ in.readInt());
+ }
+
+ @Override
+ public DeviceInfo[] newArray(int size) {
+ return new DeviceInfo[size];
+ }
+ };
+
+ @Override
+ public String toString() {
+ return new StringBuilder("DeviceInfo[")
+ .append("deviceType=").append(mDeviceType)
+ .append(", deviceName=").append(mDeviceName)
+ .append(", modelName=").append(mModelName)
+ .append(", batteryPercentage=").append(mBatteryPercentage)
+ .append(", connectionStrength=").append(mConnectionStrength)
+ .append("]").toString();
+ }
+}
diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/app/KnownNetwork.aidl b/wifi/java/src/android/net/wifi/sharedconnectivity/app/KnownNetwork.aidl
new file mode 100644
index 0000000..140d72a
--- /dev/null
+++ b/wifi/java/src/android/net/wifi/sharedconnectivity/app/KnownNetwork.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.app;
+
+parcelable KnownNetwork;
diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/app/KnownNetwork.java b/wifi/java/src/android/net/wifi/sharedconnectivity/app/KnownNetwork.java
new file mode 100644
index 0000000..34b7e94
--- /dev/null
+++ b/wifi/java/src/android/net/wifi/sharedconnectivity/app/KnownNetwork.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.app;
+
+import static android.net.wifi.WifiAnnotations.SecurityType;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * A data class representing a known Wifi network.
+ *
+ * @hide
+ */
+@SystemApi
+public final class KnownNetwork implements Parcelable {
+ /**
+ * Network is known by a nearby device with the same user account.
+ */
+ public static final int NETWORK_SOURCE_NEARBY_SELF = 0;
+
+ /**
+ * Network is known via cloud storage associated with this device's user account.
+ */
+ public static final int NETWORK_SOURCE_CLOUD_SELF = 1;
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ NETWORK_SOURCE_NEARBY_SELF,
+ NETWORK_SOURCE_CLOUD_SELF
+ })
+ public @interface NetworkSource {}
+
+ @NetworkSource private final int mNetworkSource;
+ private final String mSsid;
+ @SecurityType private final int[] mSecurityTypes;
+ private final DeviceInfo mDeviceInfo;
+
+ /**
+ * Builder class for {@link KnownNetwork}.
+ */
+ public static final class Builder {
+ @NetworkSource private int mNetworkSource = -1;
+ private String mSsid;
+ @SecurityType private int[] mSecurityTypes;
+ private android.net.wifi.sharedconnectivity.app.DeviceInfo mDeviceInfo;
+
+ public Builder() {}
+
+ /**
+ * Sets the indicated source of the known network.
+ *
+ * @param networkSource The network source as defined by IntDef {@link NetworkSource}.
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setNetworkSource(@NetworkSource int networkSource) {
+ mNetworkSource = networkSource;
+ return this;
+ }
+
+ /**
+ * Sets the SSID of the known network.
+ *
+ * @param ssid The SSID of the known network. Surrounded by double quotes if UTF-8.
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setSsid(@NonNull String ssid) {
+ mSsid = ssid;
+ return this;
+ }
+
+ /**
+ * Sets the security types of the known network.
+ *
+ * @param securityTypes The array of security types supported by the known network.
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setSecurityTypes(@NonNull @SecurityType int[] securityTypes) {
+ mSecurityTypes = securityTypes;
+ return this;
+ }
+
+ /**
+ * Sets the device information of the device providing connectivity.
+ *
+ * @param deviceInfo The array of security types supported by the known network.
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setDeviceInfo(@NonNull DeviceInfo deviceInfo) {
+ mDeviceInfo = deviceInfo;
+ return this;
+ }
+
+ /**
+ * Builds the {@link KnownNetwork} object.
+ *
+ * @return Returns the built {@link KnownNetwork} object.
+ */
+ @NonNull
+ public KnownNetwork build() {
+ return new KnownNetwork(
+ mNetworkSource,
+ mSsid,
+ mSecurityTypes,
+ mDeviceInfo);
+ }
+ }
+
+ private static void validate(int networkSource, String ssid, int [] securityTypes) {
+ if (networkSource != NETWORK_SOURCE_CLOUD_SELF && networkSource
+ != NETWORK_SOURCE_NEARBY_SELF) {
+ throw new IllegalArgumentException("Illegal network source");
+ }
+ if (TextUtils.isEmpty(ssid)) {
+ throw new IllegalArgumentException("SSID must be set");
+ }
+ if (securityTypes == null || securityTypes.length == 0) {
+ throw new IllegalArgumentException("SecurityTypes must be set");
+ }
+ }
+
+ private KnownNetwork(
+ @NetworkSource int networkSource,
+ @NonNull String ssid,
+ @NonNull @SecurityType int[] securityTypes,
+ @NonNull android.net.wifi.sharedconnectivity.app.DeviceInfo deviceInfo) {
+ validate(networkSource, ssid, securityTypes);
+ mNetworkSource = networkSource;
+ mSsid = ssid;
+ mSecurityTypes = securityTypes;
+ mDeviceInfo = deviceInfo;
+ }
+
+ /**
+ * Gets the indicated source of the known network.
+ *
+ * @return Returns the network source as defined by IntDef {@link NetworkSource}.
+ */
+ @NonNull
+ @NetworkSource
+ public int getNetworkSource() {
+ return mNetworkSource;
+ }
+
+ /**
+ * Gets the SSID of the known network.
+ *
+ * @return Returns the SSID of the known network. Surrounded by double quotes if UTF-8.
+ */
+ @NonNull
+ public String getSsid() {
+ return mSsid;
+ }
+
+ /**
+ * Gets the security types of the known network.
+ *
+ * @return Returns the array of security types supported by the known network.
+ */
+ @NonNull
+ @SecurityType
+ public int[] getSecurityTypes() {
+ return mSecurityTypes;
+ }
+
+ /**
+ * Gets the device information of the device providing connectivity.
+ *
+ * @return Returns the array of security types supported by the known network.
+ */
+ @NonNull
+ public DeviceInfo getDeviceInfo() {
+ return mDeviceInfo;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof KnownNetwork)) return false;
+ KnownNetwork other = (KnownNetwork) obj;
+ return mNetworkSource == other.getNetworkSource()
+ && Objects.equals(mSsid, other.getSsid())
+ && Arrays.equals(mSecurityTypes, other.getSecurityTypes())
+ && Objects.equals(mDeviceInfo, other.getDeviceInfo());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mNetworkSource, mSsid, Arrays.hashCode(mSecurityTypes),
+ mDeviceInfo.hashCode());
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mNetworkSource);
+ dest.writeString(mSsid);
+ dest.writeIntArray(mSecurityTypes);
+ dest.writeTypedObject(mDeviceInfo, 0);
+ }
+
+ @NonNull
+ public static final Creator<KnownNetwork> CREATOR = new Creator<>() {
+ @Override
+ public KnownNetwork createFromParcel(Parcel in) {
+ return new KnownNetwork(in.readInt(), in.readString(), in.createIntArray(),
+ in.readTypedObject(DeviceInfo.CREATOR));
+ }
+
+ @Override
+ public KnownNetwork[] newArray(int size) {
+ return new KnownNetwork[size];
+ }
+ };
+
+ @Override
+ public String toString() {
+ return new StringBuilder("KnownNetwork[")
+ .append("NetworkSource=").append(mNetworkSource)
+ .append(", ssid=").append(mSsid)
+ .append(", securityTypes=").append(Arrays.toString(mSecurityTypes))
+ .append(", deviceInfo=").append(mDeviceInfo.toString())
+ .append("]").toString();
+ }
+}
diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityClientCallback.java b/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityClientCallback.java
new file mode 100644
index 0000000..dcb5201
--- /dev/null
+++ b/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityClientCallback.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.app;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.net.wifi.sharedconnectivity.service.SharedConnectivityService;
+
+import java.util.List;
+
+/**
+ * Interface for clients of {@link SharedConnectivityManager} to register for changes in network
+ * status.
+ *
+ * @hide
+ */
+@SystemApi
+public interface SharedConnectivityClientCallback {
+ /**
+ * This method is being called by {@link SharedConnectivityService} to notify of a change in the
+ * list of available Tether Networks.
+ * @param networks Updated Tether Network list.
+ */
+ void onTetherNetworksUpdated(@NonNull List<TetherNetwork> networks);
+
+ /**
+ * This method is being called by {@link SharedConnectivityService} to notify of a change in the
+ * list of available Known Networks.
+ * @param networks Updated Known Network list.
+ */
+ void onKnownNetworksUpdated(@NonNull List<KnownNetwork> networks);
+
+ /**
+ * This method is being called by {@link SharedConnectivityService} to notify of a change in the
+ * state of share connectivity settings.
+ * @param state The new state.
+ */
+ void onSharedConnectivitySettingsChanged(@NonNull SharedConnectivitySettingsState state);
+}
+
diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManager.java b/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManager.java
new file mode 100644
index 0000000..b43e4f7
--- /dev/null
+++ b/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManager.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.app;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.res.Resources;
+import android.net.wifi.sharedconnectivity.service.ISharedConnectivityCallback;
+import android.net.wifi.sharedconnectivity.service.ISharedConnectivityService;
+import android.net.wifi.sharedconnectivity.service.SharedConnectivityService;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * This class is the library used by consumers of Shared Connectivity data to bind to the service,
+ * receive callbacks from, and send user actions to the service.
+ *
+ * @hide
+ */
+@SystemApi
+public class SharedConnectivityManager {
+ private static final String TAG = SharedConnectivityManager.class.getSimpleName();
+ private static final boolean DEBUG = true;
+ private static final String SERVICE_PACKAGE_NAME = "sharedconnectivity_service_package";
+ private static final String SERVICE_CLASS_NAME = "sharedconnectivity_service_class";
+
+ private static final class SharedConnectivityCallbackProxy extends
+ ISharedConnectivityCallback.Stub {
+ private final Executor mExecutor;
+ private final SharedConnectivityClientCallback mCallback;
+
+ SharedConnectivityCallbackProxy(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull SharedConnectivityClientCallback callback) {
+ mExecutor = executor;
+ mCallback = callback;
+ }
+
+ @Override
+ public void onTetherNetworksUpdated(@NonNull List<TetherNetwork> networks) {
+ if (mCallback != null) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> mCallback.onTetherNetworksUpdated(networks));
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ }
+
+ @Override
+ public void onKnownNetworksUpdated(@NonNull List<KnownNetwork> networks) {
+ if (mCallback != null) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> mCallback.onKnownNetworksUpdated(networks));
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ }
+
+ @Override
+ public void onSharedConnectivitySettingsChanged(
+ @NonNull SharedConnectivitySettingsState state) {
+ if (mCallback != null) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> mCallback.onSharedConnectivitySettingsChanged(state));
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ };
+ }
+
+ private ISharedConnectivityService mService;
+ private final Map<SharedConnectivityClientCallback, SharedConnectivityCallbackProxy>
+ mProxyMap = new HashMap<>();
+
+ /**
+ * Constructor for new instance of {@link SharedConnectivityManager}.
+ *
+ * Automatically binds to implementation of {@link SharedConnectivityService} specified in
+ * device overlay.
+ */
+ @SuppressLint("ManagerConstructor")
+ public SharedConnectivityManager(@NonNull Context context) {
+ ServiceConnection serviceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ mService = ISharedConnectivityService.Stub.asInterface(service);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ if (DEBUG) Log.i(TAG, "onServiceDisconnected");
+ mService = null;
+ mProxyMap.clear();
+ }
+ };
+ bind(context, serviceConnection);
+ }
+
+ /**
+ * @hide
+ */
+ @TestApi
+ public void setService(@Nullable IInterface service) {
+ mService = (ISharedConnectivityService) service;
+ }
+
+ private void bind(Context context, ServiceConnection serviceConnection) {
+ Resources resources = context.getResources();
+ int packageNameId = resources.getIdentifier(SERVICE_PACKAGE_NAME, "string",
+ context.getPackageName());
+ int classNameId = resources.getIdentifier(SERVICE_CLASS_NAME, "string",
+ context.getPackageName());
+ if (packageNameId == 0 || classNameId == 0) {
+ throw new Resources.NotFoundException("Package and class names for"
+ + " shared connectivity service must be defined");
+ }
+
+ Intent intent = new Intent();
+ intent.setComponent(new ComponentName(resources.getString(packageNameId),
+ resources.getString(classNameId)));
+ context.bindService(
+ intent,
+ serviceConnection, Context.BIND_AUTO_CREATE);
+ }
+
+ /**
+ * Registers a callback for receiving updates to the list of Tether Networks and Known Networks.
+ *
+ * @param executor The Executor used to invoke the callback.
+ * @param callback The callback of type {@link SharedConnectivityClientCallback} that is invoked
+ * when the service updates either the list of Tether Networks or Known
+ * Networks.
+ * @return Returns true if the registration was successful, false otherwise.
+ */
+ public boolean registerCallback(@NonNull @CallbackExecutor Executor executor,
+ @NonNull SharedConnectivityClientCallback callback) {
+ Objects.requireNonNull(executor, "executor cannot be null");
+ Objects.requireNonNull(callback, "callback cannot be null");
+ if (mService == null || mProxyMap.containsKey(callback)) return false;
+ try {
+ SharedConnectivityCallbackProxy proxy =
+ new SharedConnectivityCallbackProxy(executor, callback);
+ mService.registerCallback(proxy);
+ mProxyMap.put(callback, proxy);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Exception in registerCallback", e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Unregisters a callback.
+ *
+ * @return Returns true if the callback was successfully unregistered, false otherwise.
+ */
+ public boolean unregisterCallback(
+ @NonNull SharedConnectivityClientCallback callback) {
+ Objects.requireNonNull(callback, "callback cannot be null");
+ if (mService == null || !mProxyMap.containsKey(callback)) return false;
+ try {
+ mService.unregisterCallback(mProxyMap.get(callback));
+ mProxyMap.remove(callback);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Exception in unregisterCallback", e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Send command to the implementation of {@link SharedConnectivityService} requesting connection
+ * to the specified Tether Network.
+ *
+ * @param network {@link TetherNetwork} object representing the network the user has requested
+ * a connection to.
+ * @return Returns true if the service received the command. Does not guarantee that the
+ * connection was successful.
+ */
+ public boolean connectTetherNetwork(@NonNull TetherNetwork network) {
+ if (mService == null) return false;
+ try {
+ mService.connectTetherNetwork(network);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Exception in connectTetherNetwork", e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Send command to the implementation of {@link SharedConnectivityService} requesting
+ * disconnection from the active Tether Network.
+ *
+ * @return Returns true if the service received the command. Does not guarantee that the
+ * disconnection was successful.
+ */
+ public boolean disconnectTetherNetwork() {
+ if (mService == null) return false;
+ try {
+ mService.disconnectTetherNetwork();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Exception in disconnectTetherNetwork", e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Send command to the implementation of {@link SharedConnectivityService} requesting connection
+ * to the specified Known Network.
+ *
+ * @param network {@link KnownNetwork} object representing the network the user has requested
+ * a connection to.
+ * @return Returns true if the service received the command. Does not guarantee that the
+ * connection was successful.
+ */
+ public boolean connectKnownNetwork(@NonNull KnownNetwork network) {
+ if (mService == null) return false;
+ try {
+ mService.connectKnownNetwork(network);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Exception in connectKnownNetwork", e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Send command to the implementation of {@link SharedConnectivityService} requesting removal of
+ * the specified Known Network from the list of Known Networks.
+ *
+ * @return Returns true if the service received the command. Does not guarantee that the
+ * forget action was successful.
+ */
+ public boolean forgetKnownNetwork(@NonNull KnownNetwork network) {
+ if (mService == null) return false;
+ try {
+ mService.forgetKnownNetwork(network);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Exception in forgetKnownNetwork", e);
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivitySettingsState.aidl b/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivitySettingsState.aidl
new file mode 100644
index 0000000..289afac
--- /dev/null
+++ b/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivitySettingsState.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.app;
+
+parcelable SharedConnectivitySettingsState;
\ No newline at end of file
diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivitySettingsState.java b/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivitySettingsState.java
new file mode 100644
index 0000000..dd2fa94
--- /dev/null
+++ b/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivitySettingsState.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.app;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+
+/**
+ * A data class representing the shared connectivity settings state.
+ *
+ * This class represents a snapshot of the settings and can be out of date if the settings changed
+ * after receiving an object of this class.
+ *
+ * @hide
+ */
+@SystemApi
+public final class SharedConnectivitySettingsState implements Parcelable {
+
+ private final boolean mInstantTetherEnabled;
+ private final Bundle mExtras;
+
+ /**
+ * Builder class for {@link SharedConnectivitySettingsState}.
+ */
+ public static final class Builder {
+ private boolean mInstantTetherEnabled;
+ private Bundle mExtras;
+
+ public Builder() {}
+
+ /**
+ * Sets the state of Instant Tether in settings
+ *
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setInstantTetherEnabled(boolean instantTetherEnabled) {
+ mInstantTetherEnabled = instantTetherEnabled;
+ return this;
+ }
+
+ /**
+ * Sets the extras bundle
+ *
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setExtras(@NonNull Bundle extras) {
+ mExtras = extras;
+ return this;
+ }
+
+ /**
+ * Builds the {@link SharedConnectivitySettingsState} object.
+ *
+ * @return Returns the built {@link SharedConnectivitySettingsState} object.
+ */
+ @NonNull
+ public SharedConnectivitySettingsState build() {
+ return new SharedConnectivitySettingsState(mInstantTetherEnabled, mExtras);
+ }
+ }
+
+ private SharedConnectivitySettingsState(boolean instantTetherEnabled, Bundle extras) {
+ mInstantTetherEnabled = instantTetherEnabled;
+ mExtras = extras;
+ }
+
+ /**
+ * Gets the state of Instant Tether in settings
+ *
+ * @return Returns true for enabled, false otherwise.
+ */
+ @NonNull
+ public boolean isInstantTetherEnabled() {
+ return mInstantTetherEnabled;
+ }
+
+ /**
+ * Gets the extras Bundle.
+ *
+ * @return Returns a Bundle object.
+ */
+ @NonNull
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof SharedConnectivitySettingsState)) return false;
+ SharedConnectivitySettingsState other = (SharedConnectivitySettingsState) obj;
+ return mInstantTetherEnabled == other.isInstantTetherEnabled();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mInstantTetherEnabled);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeBoolean(mInstantTetherEnabled);
+ dest.writeBundle(mExtras);
+ }
+
+ @NonNull
+ public static final Creator<SharedConnectivitySettingsState> CREATOR =
+ new Creator<SharedConnectivitySettingsState>() {
+ @Override
+ public SharedConnectivitySettingsState createFromParcel(Parcel in) {
+ return new SharedConnectivitySettingsState(in.readBoolean(),
+ in.readBundle(getClass().getClassLoader()));
+ }
+
+ @Override
+ public SharedConnectivitySettingsState[] newArray(int size) {
+ return new SharedConnectivitySettingsState[size];
+ }
+ };
+
+ @Override
+ public String toString() {
+ return new StringBuilder("SharedConnectivitySettingsState[")
+ .append("instantTetherEnabled=").append(mInstantTetherEnabled)
+ .append("extras=").append(mExtras.toString())
+ .append("]").toString();
+ }
+}
diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/app/TetherNetwork.aidl b/wifi/java/src/android/net/wifi/sharedconnectivity/app/TetherNetwork.aidl
new file mode 100644
index 0000000..6cc4cfe
--- /dev/null
+++ b/wifi/java/src/android/net/wifi/sharedconnectivity/app/TetherNetwork.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.app;
+
+parcelable TetherNetwork;
diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/app/TetherNetwork.java b/wifi/java/src/android/net/wifi/sharedconnectivity/app/TetherNetwork.java
new file mode 100644
index 0000000..bbdad53
--- /dev/null
+++ b/wifi/java/src/android/net/wifi/sharedconnectivity/app/TetherNetwork.java
@@ -0,0 +1,369 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.app;
+
+import static android.net.wifi.WifiAnnotations.SecurityType;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.net.wifi.sharedconnectivity.service.SharedConnectivityService;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * A data class representing an Instant Tether network.
+ * This class is used in IPC calls between the implementer of {@link SharedConnectivityService} and
+ * the consumers of {@link com.android.wifitrackerlib}.
+ *
+ * @hide
+ */
+@SystemApi
+public final class TetherNetwork implements Parcelable {
+ /**
+ * Remote device is connected to the internet via an unknown connection.
+ */
+ public static final int NETWORK_TYPE_UNKNOWN = 0;
+
+ /**
+ * Remote device is connected to the internet via a cellular connection.
+ */
+ public static final int NETWORK_TYPE_CELLULAR = 1;
+
+ /**
+ * Remote device is connected to the internet via a Wi-Fi connection.
+ */
+ public static final int NETWORK_TYPE_WIFI = 2;
+
+ /**
+ * Remote device is connected to the internet via an ethernet connection.
+ */
+ public static final int NETWORK_TYPE_ETHERNET = 3;
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ NETWORK_TYPE_UNKNOWN,
+ NETWORK_TYPE_CELLULAR,
+ NETWORK_TYPE_WIFI,
+ NETWORK_TYPE_ETHERNET
+ })
+ public @interface NetworkType {}
+
+ private final long mDeviceId;
+ private final DeviceInfo mDeviceInfo;
+ @NetworkType private final int mNetworkType;
+ private final String mNetworkName;
+ @Nullable private final String mHotspotSsid;
+ @Nullable private final String mHotspotBssid;
+ @Nullable @SecurityType private final int[] mHotspotSecurityTypes;
+
+ /**
+ * Builder class for {@link TetherNetwork}.
+ */
+ public static final class Builder {
+ private long mDeviceId = -1;
+ private DeviceInfo mDeviceInfo;
+ @NetworkType private int mNetworkType;
+ private String mNetworkName;
+ @Nullable private String mHotspotSsid;
+ @Nullable private String mHotspotBssid;
+ @Nullable @SecurityType private int[] mHotspotSecurityTypes;
+
+ public Builder() {}
+
+ /**
+ * Set the remote device ID.
+ *
+ * @param deviceId Locally unique ID for this Instant Tether network.
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setDeviceId(long deviceId) {
+ mDeviceId = deviceId;
+ return this;
+ }
+
+ /**
+ * Sets information about the device providing connectivity.
+ *
+ * @param deviceInfo The user configurable device name.
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setDeviceInfo(@NonNull DeviceInfo deviceInfo) {
+ mDeviceInfo = deviceInfo;
+ return this;
+ }
+
+ /**
+ * Sets the network type that the remote device is connected to.
+ *
+ * @param networkType Network type as represented by IntDef {@link NetworkType}.
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setNetworkType(@NetworkType int networkType) {
+ mNetworkType = networkType;
+ return this;
+ }
+
+ /**
+ * Sets the display name of the network the remote device is connected to.
+ *
+ * @param networkName Network display name. (e.g. "Google Fi", "Hotel WiFi", "Ethernet")
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setNetworkName(@NonNull String networkName) {
+ mNetworkName = networkName;
+ return this;
+ }
+
+ /**
+ * Sets the hotspot SSID being broadcast by the remote device, or null if hotspot is off.
+ *
+ * @param hotspotSsid The SSID of the hotspot. Surrounded by double quotes if UTF-8.
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setHotspotSsid(@NonNull String hotspotSsid) {
+ mHotspotSsid = hotspotSsid;
+ return this;
+ }
+
+ /**
+ * Sets the hotspot BSSID being broadcast by the remote device, or null if hotspot is off.
+ *
+ * @param hotspotBssid The BSSID of the hotspot.
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setHotspotBssid(@NonNull String hotspotBssid) {
+ mHotspotBssid = hotspotBssid;
+ return this;
+ }
+
+ /**
+ * Sets the hotspot security types supported by the remote device, or null if hotspot is
+ * off.
+ *
+ * @param hotspotSecurityTypes The array of security types supported by the hotspot.
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setHotspotSecurityTypes(@NonNull @SecurityType int[] hotspotSecurityTypes) {
+ mHotspotSecurityTypes = hotspotSecurityTypes;
+ return this;
+ }
+
+ /**
+ * Builds the {@link TetherNetwork} object.
+ *
+ * @return Returns the built {@link TetherNetwork} object.
+ */
+ @NonNull
+ public TetherNetwork build() {
+ return new TetherNetwork(
+ mDeviceId,
+ mDeviceInfo,
+ mNetworkType,
+ mNetworkName,
+ mHotspotSsid,
+ mHotspotBssid,
+ mHotspotSecurityTypes);
+ }
+ }
+
+ private static void validate(long deviceId, int networkType, String networkName) {
+ if (deviceId < 0) {
+ throw new IllegalArgumentException("DeviceId must be set");
+ }
+ if (networkType != NETWORK_TYPE_CELLULAR && networkType != NETWORK_TYPE_WIFI
+ && networkType != NETWORK_TYPE_ETHERNET && networkType != NETWORK_TYPE_UNKNOWN) {
+ throw new IllegalArgumentException("Illegal network type");
+ }
+ if (Objects.isNull(networkName)) {
+ throw new IllegalArgumentException("NetworkName must be set");
+ }
+ }
+
+ private TetherNetwork(
+ long deviceId,
+ DeviceInfo deviceInfo,
+ @NetworkType int networkType,
+ @NonNull String networkName,
+ @Nullable String hotspotSsid,
+ @Nullable String hotspotBssid,
+ @Nullable @SecurityType int[] hotspotSecurityTypes) {
+ validate(deviceId,
+ networkType,
+ networkName);
+ mDeviceId = deviceId;
+ mDeviceInfo = deviceInfo;
+ mNetworkType = networkType;
+ mNetworkName = networkName;
+ mHotspotSsid = hotspotSsid;
+ mHotspotBssid = hotspotBssid;
+ mHotspotSecurityTypes = hotspotSecurityTypes;
+ }
+
+ /**
+ * Gets the remote device ID.
+ *
+ * @return Returns the locally unique ID for this Instant Tether network.
+ */
+ @NonNull
+ public long getDeviceId() {
+ return mDeviceId;
+ }
+
+ /**
+ * Gets information about the device providing connectivity.
+ *
+ * @return Returns the locally unique ID for this Instant Tether network.
+ */
+ @NonNull
+ public DeviceInfo getDeviceInfo() {
+ return mDeviceInfo;
+ }
+
+ /**
+ * Gets the network type that the remote device is connected to.
+ *
+ * @return Returns the network type as represented by IntDef {@link NetworkType}.
+ */
+ @NonNull
+ @NetworkType
+ public int getNetworkType() {
+ return mNetworkType;
+ }
+
+ /**
+ * Gets the display name of the network the remote device is connected to.
+ *
+ * @return Returns the network display name. (e.g. "Google Fi", "Hotel WiFi", "Ethernet")
+ */
+ @NonNull
+ public String getNetworkName() {
+ return mNetworkName;
+ }
+
+ /**
+ * Gets the hotspot SSID being broadcast by the remote device, or null if hotspot is off.
+ *
+ * @return Returns the SSID of the hotspot. Surrounded by double quotes if UTF-8.
+ */
+ @Nullable
+ public String getHotspotSsid() {
+ return mHotspotSsid;
+ }
+
+ /**
+ * Gets the hotspot BSSID being broadcast by the remote device, or null if hotspot is off.
+ *
+ * @return Returns the BSSID of the hotspot.
+ */
+ @Nullable
+ public String getHotspotBssid() {
+ return mHotspotBssid;
+ }
+
+ /**
+ * Gets the hotspot security types supported by the remote device.
+ *
+ * @return Returns the array of security types supported by the hotspot.
+ */
+ @Nullable
+ @SecurityType
+ public int[] getHotspotSecurityTypes() {
+ return mHotspotSecurityTypes;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof TetherNetwork)) return false;
+ TetherNetwork other = (TetherNetwork) obj;
+ return mDeviceId == other.getDeviceId()
+ && Objects.equals(mDeviceInfo, other.getDeviceInfo())
+ && mNetworkType == other.getNetworkType()
+ && Objects.equals(mNetworkName, other.getNetworkName())
+ && Objects.equals(mHotspotSsid, other.getHotspotSsid())
+ && Objects.equals(mHotspotBssid, other.getHotspotBssid())
+ && Arrays.equals(mHotspotSecurityTypes, other.getHotspotSecurityTypes());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mDeviceId, mDeviceInfo, mNetworkName, mHotspotSsid, mHotspotBssid,
+ Arrays.hashCode(mHotspotSecurityTypes));
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeLong(mDeviceId);
+ dest.writeTypedObject(mDeviceInfo, 0);
+ dest.writeInt(mNetworkType);
+ dest.writeString(mNetworkName);
+ dest.writeString(mHotspotSsid);
+ dest.writeString(mHotspotBssid);
+ dest.writeIntArray(mHotspotSecurityTypes);
+ }
+
+ @NonNull
+ public static final Creator<TetherNetwork> CREATOR = new Creator<>() {
+ @Override
+ public TetherNetwork createFromParcel(Parcel in) {
+ return new TetherNetwork(in.readLong(), in.readTypedObject(
+ android.net.wifi.sharedconnectivity.app.DeviceInfo.CREATOR),
+ in.readInt(), in.readString(), in.readString(), in.readString(),
+ in.createIntArray());
+ }
+
+ @Override
+ public TetherNetwork[] newArray(int size) {
+ return new TetherNetwork[size];
+ }
+ };
+
+ @Override
+ public String toString() {
+ return new StringBuilder("TetherNetwork[")
+ .append("deviceId=").append(mDeviceId)
+ .append(", networkType=").append(mNetworkType)
+ .append(", deviceInfo=").append(mDeviceInfo.toString())
+ .append(", networkName=").append(mNetworkName)
+ .append(", hotspotSsid=").append(mHotspotSsid)
+ .append(", hotspotBssid=").append(mHotspotBssid)
+ .append(", hotspotSecurityTypes=").append(Arrays.toString(mHotspotSecurityTypes))
+ .append("]").toString();
+ }
+}
diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/service/ISharedConnectivityCallback.aidl b/wifi/java/src/android/net/wifi/sharedconnectivity/service/ISharedConnectivityCallback.aidl
new file mode 100644
index 0000000..6e56138
--- /dev/null
+++ b/wifi/java/src/android/net/wifi/sharedconnectivity/service/ISharedConnectivityCallback.aidl
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.service;
+
+import android.net.wifi.sharedconnectivity.app.KnownNetwork;
+import android.net.wifi.sharedconnectivity.app.SharedConnectivitySettingsState;
+import android.net.wifi.sharedconnectivity.app.TetherNetwork;
+
+/*
+ * @hide
+ */
+interface ISharedConnectivityCallback {
+ oneway void onTetherNetworksUpdated(in List<TetherNetwork> networks);
+ oneway void onKnownNetworksUpdated(in List<KnownNetwork> networks);
+ oneway void onSharedConnectivitySettingsChanged(in SharedConnectivitySettingsState state);
+}
diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/service/ISharedConnectivityService.aidl b/wifi/java/src/android/net/wifi/sharedconnectivity/service/ISharedConnectivityService.aidl
new file mode 100644
index 0000000..5d79405
--- /dev/null
+++ b/wifi/java/src/android/net/wifi/sharedconnectivity/service/ISharedConnectivityService.aidl
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.service;
+
+import android.net.wifi.sharedconnectivity.app.KnownNetwork;
+import android.net.wifi.sharedconnectivity.app.TetherNetwork;
+import android.net.wifi.sharedconnectivity.service.ISharedConnectivityCallback;
+
+/*
+ * @hide
+ */
+interface ISharedConnectivityService {
+ void registerCallback(in ISharedConnectivityCallback callback);
+ void unregisterCallback(in ISharedConnectivityCallback callback);
+ void connectTetherNetwork(in TetherNetwork network);
+ void disconnectTetherNetwork();
+ void connectKnownNetwork(in KnownNetwork network);
+ void forgetKnownNetwork(in KnownNetwork network);
+}
diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/service/SharedConnectivityService.java b/wifi/java/src/android/net/wifi/sharedconnectivity/service/SharedConnectivityService.java
new file mode 100644
index 0000000..234319a
--- /dev/null
+++ b/wifi/java/src/android/net/wifi/sharedconnectivity/service/SharedConnectivityService.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.service;
+
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.Manifest.permission.NETWORK_SETUP_WIZARD;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.wifi.sharedconnectivity.app.KnownNetwork;
+import android.net.wifi.sharedconnectivity.app.SharedConnectivityManager;
+import android.net.wifi.sharedconnectivity.app.SharedConnectivitySettingsState;
+import android.net.wifi.sharedconnectivity.app.TetherNetwork;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * This class is the partly implemented service for injecting Shared Connectivity networks into the
+ * Wi-Fi Pickers and other relevant UI surfaces.
+ *
+ * Implementing application should extend this service and override the indicated methods.
+ * Callers to the service should use {@link SharedConnectivityManager} to bind to the implemented
+ * service as specified in the configuration overlay.
+ *
+ * @hide
+ */
+@SystemApi
+public abstract class SharedConnectivityService extends Service {
+ private static final String TAG = SharedConnectivityService.class.getSimpleName();
+ private static final boolean DEBUG = true;
+
+ private final Handler mHandler;
+ private final List<ISharedConnectivityCallback> mCallbacks = new ArrayList<>();
+ // Used to find DeathRecipient when unregistering a callback to call unlinkToDeath.
+ private final Map<ISharedConnectivityCallback, DeathRecipient> mDeathRecipientMap =
+ new HashMap<>();
+
+ private List<TetherNetwork> mTetherNetworks = Collections.emptyList();
+ private List<KnownNetwork> mKnownNetworks = Collections.emptyList();
+ private SharedConnectivitySettingsState mSettingsState;
+
+ public SharedConnectivityService() {
+ mHandler = new Handler(getMainLooper());
+ }
+
+ public SharedConnectivityService(@NonNull Handler handler) {
+ mHandler = handler;
+ }
+
+ private final class DeathRecipient implements IBinder.DeathRecipient {
+ ISharedConnectivityCallback mCallback;
+
+ DeathRecipient(ISharedConnectivityCallback callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void binderDied() {
+ mCallbacks.remove(mCallback);
+ mDeathRecipientMap.remove(mCallback);
+ }
+ }
+
+ @Override
+ @Nullable
+ public IBinder onBind(@NonNull Intent intent) {
+ if (DEBUG) Log.i(TAG, "onBind intent=" + intent);
+ return new ISharedConnectivityService.Stub() {
+ @Override
+ public void registerCallback(ISharedConnectivityCallback callback) {
+ checkPermissions();
+ mHandler.post(() -> registerCallback(callback));
+ }
+
+ @Override
+ public void unregisterCallback(ISharedConnectivityCallback callback) {
+ checkPermissions();
+ mHandler.post(() -> unregisterCallback(callback));
+ }
+
+ @Override
+ public void connectTetherNetwork(TetherNetwork network) {
+ checkPermissions();
+ mHandler.post(() -> onConnectTetherNetwork(network));
+ }
+
+ @Override
+ public void disconnectTetherNetwork() {
+ checkPermissions();
+ mHandler.post(() -> onDisconnectTetherNetwork());
+ }
+
+ @Override
+ public void connectKnownNetwork(KnownNetwork network) {
+ checkPermissions();
+ mHandler.post(() -> onConnectKnownNetwork(network));
+ }
+
+ @Override
+ public void forgetKnownNetwork(KnownNetwork network) {
+ checkPermissions();
+ mHandler.post(() -> onForgetKnownNetwork(network));
+ }
+
+ @RequiresPermission(anyOf = {android.Manifest.permission.NETWORK_SETTINGS,
+ android.Manifest.permission.NETWORK_SETUP_WIZARD})
+ private void checkPermissions() {
+ if (checkCallingPermission(NETWORK_SETTINGS) != PackageManager.PERMISSION_GRANTED
+ && checkCallingPermission(NETWORK_SETUP_WIZARD)
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Calling process must have NETWORK_SETTINGS or"
+ + " NETWORK_SETUP_WIZARD permission");
+ }
+ }
+ };
+ }
+
+ private void registerCallback(ISharedConnectivityCallback callback) {
+ // Listener gets triggered on first register using cashed data
+ if (!notifyTetherNetworkUpdate(callback) || !notifyKnownNetworkUpdate(callback)) {
+ if (DEBUG) Log.w(TAG, "Failed to notify client");
+ return;
+ }
+
+ DeathRecipient deathRecipient = new DeathRecipient(callback);
+ try {
+ callback.asBinder().linkToDeath(deathRecipient, 0);
+ mCallbacks.add(callback);
+ mDeathRecipientMap.put(callback, deathRecipient);
+ } catch (RemoteException e) {
+ if (DEBUG) Log.w(TAG, "Exception in registerCallback", e);
+ }
+ }
+
+ private void unregisterCallback(ISharedConnectivityCallback callback) {
+ DeathRecipient deathRecipient = mDeathRecipientMap.get(callback);
+ if (deathRecipient != null) {
+ callback.asBinder().unlinkToDeath(deathRecipient, 0);
+ mDeathRecipientMap.remove(callback);
+ }
+ mCallbacks.remove(callback);
+ }
+
+ private boolean notifyTetherNetworkUpdate(ISharedConnectivityCallback callback) {
+ try {
+ callback.onTetherNetworksUpdated(mTetherNetworks);
+ } catch (RemoteException e) {
+ if (DEBUG) Log.w(TAG, "Exception in notifyTetherNetworkUpdate", e);
+ return false;
+ }
+ return true;
+ }
+
+ private boolean notifyKnownNetworkUpdate(ISharedConnectivityCallback callback) {
+ try {
+ callback.onKnownNetworksUpdated(mKnownNetworks);
+ } catch (RemoteException e) {
+ if (DEBUG) Log.w(TAG, "Exception in notifyKnownNetworkUpdate", e);
+ return false;
+ }
+ return true;
+ }
+
+ private boolean notifySettingsStateUpdate(ISharedConnectivityCallback callback) {
+ try {
+ callback.onSharedConnectivitySettingsChanged(mSettingsState);
+ } catch (RemoteException e) {
+ if (DEBUG) Log.w(TAG, "Exception in notifySettingsStateUpdate", e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Implementing application should call this method to provide an up-to-date list of Tether
+ * Networks to be displayed to the user.
+ *
+ * This method updates the cached list and notifies all registered callbacks. Any callbacks that
+ * are inaccessible will be unregistered.
+ *
+ * @param networks The updated list of {@link TetherNetwork} objects.
+ */
+ public final void setTetherNetworks(@NonNull List<TetherNetwork> networks) {
+ mTetherNetworks = networks;
+
+ for (ISharedConnectivityCallback callback:mCallbacks) {
+ notifyTetherNetworkUpdate(callback);
+ }
+ }
+
+ /**
+ * Implementing application should call this method to provide an up-to-date list of Known
+ * Networks to be displayed to the user.
+ *
+ * This method updates the cached list and notifies all registered callbacks. Any callbacks that
+ * are inaccessible will be unregistered.
+ *
+ * @param networks The updated list of {@link KnownNetwork} objects.
+ */
+ public final void setKnownNetworks(@NonNull List<KnownNetwork> networks) {
+ mKnownNetworks = networks;
+
+ for (ISharedConnectivityCallback callback:mCallbacks) {
+ notifyKnownNetworkUpdate(callback);
+ }
+ }
+
+ /**
+ * Implementing application should call this method to provide an up-to-date state of Shared
+ * connectivity settings state.
+ *
+ * This method updates the cached state and notifies all registered callbacks. Any callbacks
+ * that are inaccessible will be unregistered.
+ *
+ * @param settingsState The updated state {@link SharedConnectivitySettingsState}
+ * objects.
+ */
+ public final void setSettingsState(@NonNull SharedConnectivitySettingsState settingsState) {
+ mSettingsState = settingsState;
+
+ for (ISharedConnectivityCallback callback:mCallbacks) {
+ notifySettingsStateUpdate(callback);
+ }
+ }
+
+ /**
+ * Implementing application should implement this method.
+ *
+ * Implementation should initiate a connection to the Tether Network indicated.
+ *
+ * @param network Object identifying the Tether Network the user has requested a connection to.
+ */
+ public abstract void onConnectTetherNetwork(@NonNull TetherNetwork network);
+
+ /**
+ * Implementing application should implement this method.
+ *
+ * Implementation should initiate a disconnection from the active Tether Network.
+ */
+ public abstract void onDisconnectTetherNetwork();
+
+ /**
+ * Implementing application should implement this method.
+ *
+ * Implementation should initiate a connection to the Known Network indicated.
+ *
+ * @param network Object identifying the Known Network the user has requested a connection to.
+ */
+ public abstract void onConnectKnownNetwork(@NonNull KnownNetwork network);
+
+ /**
+ * Implementing application should implement this method.
+ *
+ * Implementation should remove the Known Network indicated from the synced list of networks.
+ *
+ * @param network Object identifying the Known Network the user has requested to forget.
+ */
+ public abstract void onForgetKnownNetwork(@NonNull KnownNetwork network);
+}
diff --git a/wifi/tests/src/android/net/wifi/sharedconnectivity/app/DeviceInfoTest.java b/wifi/tests/src/android/net/wifi/sharedconnectivity/app/DeviceInfoTest.java
new file mode 100644
index 0000000..f8f0700
--- /dev/null
+++ b/wifi/tests/src/android/net/wifi/sharedconnectivity/app/DeviceInfoTest.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.app;
+
+import static android.net.wifi.sharedconnectivity.app.DeviceInfo.DEVICE_TYPE_LAPTOP;
+import static android.net.wifi.sharedconnectivity.app.DeviceInfo.DEVICE_TYPE_PHONE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.os.Parcel;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link android.app.sharedconnectivity.DeviceInfo}.
+ */
+@SmallTest
+public class DeviceInfoTest {
+
+ private static final int DEVICE_TYPE = DEVICE_TYPE_PHONE;
+ private static final String DEVICE_NAME = "TEST_NAME";
+ private static final String DEVICE_MODEL = "TEST_MODEL";
+ private static final int BATTERY_PERCENTAGE = 50;
+ private static final int CONNECTION_STRENGTH = 2;
+
+ private static final int DEVICE_TYPE_1 = DEVICE_TYPE_LAPTOP;
+ private static final String DEVICE_NAME_1 = "TEST_NAME1";
+ private static final String DEVICE_MODEL_1 = "TEST_MODEL1";
+ private static final int BATTERY_PERCENTAGE_1 = 30;
+ private static final int CONNECTION_STRENGTH_1 = 1;
+
+ /**
+ * Verifies parcel serialization/deserialization.
+ */
+ @Test
+ public void testParcelOperation() {
+ DeviceInfo info = buildDeviceInfoBuilder().build();
+
+ Parcel parcelW = Parcel.obtain();
+ info.writeToParcel(parcelW, 0);
+ byte[] bytes = parcelW.marshall();
+ parcelW.recycle();
+
+ Parcel parcelR = Parcel.obtain();
+ parcelR.unmarshall(bytes, 0, bytes.length);
+ parcelR.setDataPosition(0);
+ DeviceInfo fromParcel = DeviceInfo.CREATOR.createFromParcel(parcelR);
+
+ assertEquals(info, fromParcel);
+ assertEquals(info.hashCode(), fromParcel.hashCode());
+ }
+
+ /**
+ * Verifies the Equals operation
+ */
+ @Test
+ public void testEqualsOperation() {
+ DeviceInfo info1 = buildDeviceInfoBuilder().build();
+ DeviceInfo info2 = buildDeviceInfoBuilder().build();
+ assertEquals(info1, info2);
+
+ DeviceInfo.Builder builder = buildDeviceInfoBuilder().setDeviceType(DEVICE_TYPE_1);
+ assertNotEquals(info1, builder.build());
+
+ builder = buildDeviceInfoBuilder().setDeviceName(DEVICE_NAME_1);
+ assertNotEquals(info1, builder.build());
+
+ builder = buildDeviceInfoBuilder().setModelName(DEVICE_MODEL_1);
+ assertNotEquals(info1, builder.build());
+
+ builder = buildDeviceInfoBuilder()
+ .setBatteryPercentage(BATTERY_PERCENTAGE_1);
+ assertNotEquals(info1, builder.build());
+
+ builder = buildDeviceInfoBuilder()
+ .setConnectionStrength(CONNECTION_STRENGTH_1);
+ assertNotEquals(info1, builder.build());
+ }
+
+ /**
+ * Verifies the get methods return the expected data.
+ */
+ @Test
+ public void testGetMethods() {
+ DeviceInfo info = buildDeviceInfoBuilder().build();
+ assertEquals(info.getDeviceType(), DEVICE_TYPE);
+ assertEquals(info.getDeviceName(), DEVICE_NAME);
+ assertEquals(info.getModelName(), DEVICE_MODEL);
+ assertEquals(info.getBatteryPercentage(), BATTERY_PERCENTAGE);
+ assertEquals(info.getConnectionStrength(), CONNECTION_STRENGTH);
+ assertEquals(info.getConnectionStrength(), CONNECTION_STRENGTH);
+ }
+
+ private DeviceInfo.Builder buildDeviceInfoBuilder() {
+ return new DeviceInfo.Builder().setDeviceType(DEVICE_TYPE).setDeviceName(DEVICE_NAME)
+ .setModelName(DEVICE_MODEL).setBatteryPercentage(BATTERY_PERCENTAGE)
+ .setConnectionStrength(CONNECTION_STRENGTH);
+ }
+}
diff --git a/wifi/tests/src/android/net/wifi/sharedconnectivity/app/KnownNetworkTest.java b/wifi/tests/src/android/net/wifi/sharedconnectivity/app/KnownNetworkTest.java
new file mode 100644
index 0000000..266afcc
--- /dev/null
+++ b/wifi/tests/src/android/net/wifi/sharedconnectivity/app/KnownNetworkTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.app;
+
+import static android.net.wifi.WifiInfo.SECURITY_TYPE_PSK;
+import static android.net.wifi.WifiInfo.SECURITY_TYPE_WEP;
+import static android.net.wifi.sharedconnectivity.app.DeviceInfo.DEVICE_TYPE_PHONE;
+import static android.net.wifi.sharedconnectivity.app.DeviceInfo.DEVICE_TYPE_TABLET;
+import static android.net.wifi.sharedconnectivity.app.KnownNetwork.NETWORK_SOURCE_CLOUD_SELF;
+import static android.net.wifi.sharedconnectivity.app.KnownNetwork.NETWORK_SOURCE_NEARBY_SELF;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.os.Parcel;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link android.app.sharedconnectivity.KnownNetwork}.
+ */
+@SmallTest
+public class KnownNetworkTest {
+
+ private static final int NETWORK_SOURCE = NETWORK_SOURCE_NEARBY_SELF;
+ private static final String SSID = "TEST_SSID";
+ private static final int[] SECURITY_TYPES = {SECURITY_TYPE_WEP};
+ private static final DeviceInfo DEVICE_INFO = new DeviceInfo.Builder()
+ .setDeviceType(DEVICE_TYPE_TABLET).setDeviceName("TEST_NAME").setModelName("TEST_MODEL")
+ .setConnectionStrength(2).setBatteryPercentage(50).build();
+ private static final int NETWORK_SOURCE_1 = NETWORK_SOURCE_CLOUD_SELF;
+ private static final String SSID_1 = "TEST_SSID1";
+ private static final int[] SECURITY_TYPES_1 = {SECURITY_TYPE_PSK};
+ private static final DeviceInfo DEVICE_INFO_1 = new DeviceInfo.Builder()
+ .setDeviceType(DEVICE_TYPE_PHONE).setDeviceName("TEST_NAME_1")
+ .setModelName("TEST_MODEL_1").setConnectionStrength(3).setBatteryPercentage(33).build();
+
+ /**
+ * Verifies parcel serialization/deserialization.
+ */
+ @Test
+ public void testParcelOperation() {
+ KnownNetwork network = buildKnownNetworkBuilder().build();
+
+ Parcel parcelW = Parcel.obtain();
+ network.writeToParcel(parcelW, 0);
+ byte[] bytes = parcelW.marshall();
+ parcelW.recycle();
+
+ Parcel parcelR = Parcel.obtain();
+ parcelR.unmarshall(bytes, 0, bytes.length);
+ parcelR.setDataPosition(0);
+ KnownNetwork fromParcel = KnownNetwork.CREATOR.createFromParcel(parcelR);
+
+ assertEquals(network, fromParcel);
+ assertEquals(network.hashCode(), fromParcel.hashCode());
+ }
+
+ /**
+ * Verifies the Equals operation
+ */
+ @Test
+ public void testEqualsOperation() {
+ KnownNetwork network1 = buildKnownNetworkBuilder().build();
+ KnownNetwork network2 = buildKnownNetworkBuilder().build();
+ assertEquals(network1, network2);
+
+ KnownNetwork.Builder builder = buildKnownNetworkBuilder()
+ .setNetworkSource(NETWORK_SOURCE_1);
+ assertNotEquals(network1, builder.build());
+
+ builder = buildKnownNetworkBuilder().setSsid(SSID_1);
+ assertNotEquals(network1, builder.build());
+
+ builder = buildKnownNetworkBuilder().setSecurityTypes(SECURITY_TYPES_1);
+ assertNotEquals(network1, builder.build());
+
+ builder = buildKnownNetworkBuilder().setDeviceInfo(DEVICE_INFO_1);
+ assertNotEquals(network1, builder.build());
+ }
+
+ /**
+ * Verifies the get methods return the expected data.
+ */
+ @Test
+ public void testGetMethods() {
+ KnownNetwork network = buildKnownNetworkBuilder().build();
+ assertEquals(network.getNetworkSource(), NETWORK_SOURCE);
+ assertEquals(network.getSsid(), SSID);
+ assertArrayEquals(network.getSecurityTypes(), SECURITY_TYPES);
+ assertEquals(network.getDeviceInfo(), DEVICE_INFO);
+ }
+
+ private KnownNetwork.Builder buildKnownNetworkBuilder() {
+ return new KnownNetwork.Builder().setNetworkSource(NETWORK_SOURCE).setSsid(SSID)
+ .setSecurityTypes(SECURITY_TYPES).setDeviceInfo(DEVICE_INFO);
+ }
+}
diff --git a/wifi/tests/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManagerTest.java b/wifi/tests/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManagerTest.java
new file mode 100644
index 0000000..9aeccac
--- /dev/null
+++ b/wifi/tests/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManagerTest.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.app;
+
+import static android.net.wifi.WifiInfo.SECURITY_TYPE_EAP;
+import static android.net.wifi.WifiInfo.SECURITY_TYPE_WEP;
+import static android.net.wifi.sharedconnectivity.app.DeviceInfo.DEVICE_TYPE_TABLET;
+import static android.net.wifi.sharedconnectivity.app.KnownNetwork.NETWORK_SOURCE_NEARBY_SELF;
+import static android.net.wifi.sharedconnectivity.app.TetherNetwork.NETWORK_TYPE_CELLULAR;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.wifi.sharedconnectivity.service.ISharedConnectivityService;
+import android.os.RemoteException;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Unit tests for {@link SharedConnectivityManager}.
+ */
+@SmallTest
+public class SharedConnectivityManagerTest {
+ private static final long DEVICE_ID = 11L;
+ private static final DeviceInfo DEVICE_INFO = new DeviceInfo.Builder()
+ .setDeviceType(DEVICE_TYPE_TABLET).setDeviceName("TEST_NAME").setModelName("TEST_MODEL")
+ .setConnectionStrength(2).setBatteryPercentage(50).build();
+ private static final int NETWORK_TYPE = NETWORK_TYPE_CELLULAR;
+ private static final String NETWORK_NAME = "TEST_NETWORK";
+ private static final String HOTSPOT_SSID = "TEST_SSID";
+ private static final int[] HOTSPOT_SECURITY_TYPES = {SECURITY_TYPE_WEP, SECURITY_TYPE_EAP};
+
+ private static final int NETWORK_SOURCE = NETWORK_SOURCE_NEARBY_SELF;
+ private static final String SSID = "TEST_SSID";
+ private static final int[] SECURITY_TYPES = {SECURITY_TYPE_WEP};
+
+ private static final int SERVICE_PACKAGE_ID = 1;
+ private static final int SERVICE_CLASS_ID = 2;
+
+ private static final String SERVICE_PACKAGE_NAME = "TEST_PACKAGE";
+ private static final String SERVICE_CLASS_NAME = "TEST_CLASS";
+ private static final String PACKAGE_NAME = "TEST_PACKAGE";
+
+ @Mock Context mContext;
+ @Mock
+ ISharedConnectivityService mService;
+ @Mock Executor mExecutor;
+ @Mock
+ SharedConnectivityClientCallback mClientCallback;
+ @Mock Resources mResources;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ setResources(mContext);
+ }
+
+ /**
+ * Verifies constructor is binding to service.
+ */
+ @Test
+ public void testBindingToService() {
+ new SharedConnectivityManager(mContext);
+ verify(mContext).bindService(any(), any(), anyInt());
+ }
+
+ /**
+ * Verifies callback is registered in the service only once and only when service is not null.
+ */
+ @Test
+ public void testRegisterCallback() throws Exception {
+ SharedConnectivityManager manager = new SharedConnectivityManager(mContext);
+ manager.setService(null);
+ assertFalse(manager.registerCallback(mExecutor, mClientCallback));
+
+ manager = new SharedConnectivityManager(mContext);
+ manager.setService(mService);
+ assertTrue(manager.registerCallback(mExecutor, mClientCallback));
+ verify(mService).registerCallback(any());
+
+ // Registering the same callback twice should fail.
+ manager = new SharedConnectivityManager(mContext);
+ manager.setService(mService);
+ manager.registerCallback(mExecutor, mClientCallback);
+ assertFalse(manager.registerCallback(mExecutor, mClientCallback));
+
+ manager = new SharedConnectivityManager(mContext);
+ manager.setService(mService);
+ doThrow(new RemoteException()).when(mService).registerCallback(any());
+ assertFalse(manager.registerCallback(mExecutor, mClientCallback));
+ }
+
+ /**
+ * Verifies callback is unregistered from the service if it was registered before and only when
+ * service is not null.
+ */
+ @Test
+ public void testUnregisterCallback() throws Exception {
+ SharedConnectivityManager manager = new SharedConnectivityManager(mContext);
+ manager.setService(null);
+ assertFalse(manager.unregisterCallback(mClientCallback));
+
+ manager = new SharedConnectivityManager(mContext);
+ manager.setService(mService);
+ manager.registerCallback(mExecutor, mClientCallback);
+ assertTrue(manager.unregisterCallback(mClientCallback));
+ verify(mService).unregisterCallback(any());
+
+
+ manager = new SharedConnectivityManager(mContext);
+ manager.setService(mService);
+ manager.registerCallback(mExecutor, mClientCallback);
+ manager.unregisterCallback(mClientCallback);
+ assertFalse(manager.unregisterCallback(mClientCallback));
+
+ manager = new SharedConnectivityManager(mContext);
+ manager.setService(mService);
+ doThrow(new RemoteException()).when(mService).unregisterCallback(any());
+ assertFalse(manager.unregisterCallback(mClientCallback));
+ }
+
+ /**
+ * Verifies service is called when not null and exceptions are handles when calling
+ * connectTetherNetwork.
+ */
+ @Test
+ public void testConnectTetherNetwork() throws RemoteException {
+ TetherNetwork network = buildTetherNetwork();
+
+ SharedConnectivityManager manager = new SharedConnectivityManager(mContext);
+ manager.setService(null);
+ assertFalse(manager.connectTetherNetwork(network));
+
+ manager = new SharedConnectivityManager(mContext);
+ manager.setService(mService);
+ manager.connectTetherNetwork(network);
+ verify(mService).connectTetherNetwork(network);
+
+ doThrow(new RemoteException()).when(mService).connectTetherNetwork(network);
+ assertFalse(manager.connectTetherNetwork(network));
+ }
+
+ /**
+ * Verifies service is called when not null and exceptions are handles when calling
+ * disconnectTetherNetwork.
+ */
+ @Test
+ public void testDisconnectTetherNetwork() throws RemoteException {
+ SharedConnectivityManager manager = new SharedConnectivityManager(mContext);
+ manager.setService(null);
+ assertFalse(manager.disconnectTetherNetwork());
+
+ manager = new SharedConnectivityManager(mContext);
+ manager.setService(mService);
+ manager.disconnectTetherNetwork();
+ verify(mService).disconnectTetherNetwork();
+
+ doThrow(new RemoteException()).when(mService).disconnectTetherNetwork();
+ assertFalse(manager.disconnectTetherNetwork());
+ }
+
+ /**
+ * Verifies service is called when not null and exceptions are handles when calling
+ * connectKnownNetwork.
+ */
+ @Test
+ public void testConnectKnownNetwork() throws RemoteException {
+ KnownNetwork network = buildKnownNetwork();
+
+ SharedConnectivityManager manager = new SharedConnectivityManager(mContext);
+ manager.setService(null);
+ assertFalse(manager.connectKnownNetwork(network));
+
+ manager = new SharedConnectivityManager(mContext);
+ manager.setService(mService);
+ manager.connectKnownNetwork(network);
+ verify(mService).connectKnownNetwork(network);
+
+ doThrow(new RemoteException()).when(mService).connectKnownNetwork(network);
+ assertFalse(manager.connectKnownNetwork(network));
+ }
+
+ /**
+ * Verifies service is called when not null and exceptions are handles when calling
+ * forgetKnownNetwork.
+ */
+ @Test
+ public void testForgetKnownNetwork() throws RemoteException {
+ KnownNetwork network = buildKnownNetwork();
+
+ SharedConnectivityManager manager = new SharedConnectivityManager(mContext);
+ manager.setService(null);
+ assertFalse(manager.forgetKnownNetwork(network));
+
+ manager = new SharedConnectivityManager(mContext);
+ manager.setService(mService);
+ manager.forgetKnownNetwork(network);
+ verify(mService).forgetKnownNetwork(network);
+
+ doThrow(new RemoteException()).when(mService).forgetKnownNetwork(network);
+ assertFalse(manager.forgetKnownNetwork(network));
+ }
+
+ private void setResources(@Mock Context context) {
+ when(context.getResources()).thenReturn(mResources);
+ when(context.getPackageName()).thenReturn(PACKAGE_NAME);
+ when(mResources.getIdentifier(anyString(), anyString(), anyString()))
+ .thenReturn(SERVICE_PACKAGE_ID, SERVICE_CLASS_ID);
+ when(mResources.getString(SERVICE_PACKAGE_ID)).thenReturn(SERVICE_PACKAGE_NAME);
+ when(mResources.getString(SERVICE_CLASS_ID)).thenReturn(SERVICE_CLASS_NAME);
+ }
+
+ private TetherNetwork buildTetherNetwork() {
+ return new TetherNetwork.Builder()
+ .setDeviceId(DEVICE_ID)
+ .setDeviceInfo(DEVICE_INFO)
+ .setNetworkType(NETWORK_TYPE)
+ .setNetworkName(NETWORK_NAME)
+ .setHotspotSsid(HOTSPOT_SSID)
+ .setHotspotSecurityTypes(HOTSPOT_SECURITY_TYPES)
+ .build();
+ }
+
+ private KnownNetwork buildKnownNetwork() {
+ return new KnownNetwork.Builder().setNetworkSource(NETWORK_SOURCE).setSsid(SSID)
+ .setSecurityTypes(SECURITY_TYPES).build();
+ }
+}
diff --git a/wifi/tests/src/android/net/wifi/sharedconnectivity/app/SharedConnectivitySettingsStateTest.java b/wifi/tests/src/android/net/wifi/sharedconnectivity/app/SharedConnectivitySettingsStateTest.java
new file mode 100644
index 0000000..3137c72
--- /dev/null
+++ b/wifi/tests/src/android/net/wifi/sharedconnectivity/app/SharedConnectivitySettingsStateTest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.app;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.os.Parcel;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link android.net.wifi.sharedconnectivity.app.SharedConnectivitySettingsState}.
+ */
+@SmallTest
+public class SharedConnectivitySettingsStateTest {
+ private static final boolean INSTANT_TETHER_STATE = true;
+
+ private static final boolean INSTANT_TETHER_STATE_1 = false;
+ /**
+ * Verifies parcel serialization/deserialization.
+ */
+ @Test
+ public void testParcelOperation() {
+ SharedConnectivitySettingsState state = buildSettingsStateBuilder().build();
+
+ Parcel parcelW = Parcel.obtain();
+ state.writeToParcel(parcelW, 0);
+ byte[] bytes = parcelW.marshall();
+ parcelW.recycle();
+
+ Parcel parcelR = Parcel.obtain();
+ parcelR.unmarshall(bytes, 0, bytes.length);
+ parcelR.setDataPosition(0);
+ SharedConnectivitySettingsState fromParcel =
+ SharedConnectivitySettingsState.CREATOR.createFromParcel(parcelR);
+
+ assertEquals(state, fromParcel);
+ assertEquals(state.hashCode(), fromParcel.hashCode());
+ }
+
+ /**
+ * Verifies the Equals operation
+ */
+ @Test
+ public void testEqualsOperation() {
+ SharedConnectivitySettingsState state1 = buildSettingsStateBuilder().build();
+ SharedConnectivitySettingsState state2 = buildSettingsStateBuilder().build();
+ assertEquals(state1, state2);
+
+ SharedConnectivitySettingsState.Builder builder = buildSettingsStateBuilder()
+ .setInstantTetherEnabled(INSTANT_TETHER_STATE_1);
+ assertNotEquals(state1, builder.build());
+ }
+
+ /**
+ * Verifies the get methods return the expected data.
+ */
+ @Test
+ public void testGetMethods() {
+ SharedConnectivitySettingsState state = buildSettingsStateBuilder().build();
+ assertEquals(state.isInstantTetherEnabled(), INSTANT_TETHER_STATE);
+ }
+
+ private SharedConnectivitySettingsState.Builder buildSettingsStateBuilder() {
+ return new SharedConnectivitySettingsState.Builder()
+ .setInstantTetherEnabled(INSTANT_TETHER_STATE);
+ }
+}
diff --git a/wifi/tests/src/android/net/wifi/sharedconnectivity/app/TetherNetworkTest.java b/wifi/tests/src/android/net/wifi/sharedconnectivity/app/TetherNetworkTest.java
new file mode 100644
index 0000000..b01aec4
--- /dev/null
+++ b/wifi/tests/src/android/net/wifi/sharedconnectivity/app/TetherNetworkTest.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.app;
+
+import static android.net.wifi.WifiInfo.SECURITY_TYPE_EAP;
+import static android.net.wifi.WifiInfo.SECURITY_TYPE_PSK;
+import static android.net.wifi.WifiInfo.SECURITY_TYPE_WEP;
+import static android.net.wifi.sharedconnectivity.app.DeviceInfo.DEVICE_TYPE_PHONE;
+import static android.net.wifi.sharedconnectivity.app.DeviceInfo.DEVICE_TYPE_TABLET;
+import static android.net.wifi.sharedconnectivity.app.TetherNetwork.NETWORK_TYPE_CELLULAR;
+import static android.net.wifi.sharedconnectivity.app.TetherNetwork.NETWORK_TYPE_WIFI;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.os.Parcel;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link android.net.wifi.sharedconnectivity.app.TetherNetwork}.
+ */
+@SmallTest
+public class TetherNetworkTest {
+ private static final long DEVICE_ID = 11L;
+ private static final DeviceInfo DEVICE_INFO = new DeviceInfo.Builder()
+ .setDeviceType(DEVICE_TYPE_TABLET).setDeviceName("TEST_NAME").setModelName("TEST_MODEL")
+ .setConnectionStrength(2).setBatteryPercentage(50).build();
+ private static final int NETWORK_TYPE = NETWORK_TYPE_CELLULAR;
+ private static final String NETWORK_NAME = "TEST_NETWORK";
+ private static final String HOTSPOT_SSID = "TEST_SSID";
+ private static final String HOTSPOT_BSSID = "TEST _BSSID";
+ private static final int[] HOTSPOT_SECURITY_TYPES = {SECURITY_TYPE_WEP, SECURITY_TYPE_EAP};
+
+ private static final long DEVICE_ID_1 = 111L;
+ private static final DeviceInfo DEVICE_INFO_1 = new DeviceInfo.Builder()
+ .setDeviceType(DEVICE_TYPE_PHONE).setDeviceName("TEST_NAME").setModelName("TEST_MODEL")
+ .setConnectionStrength(2).setBatteryPercentage(50).build();
+ private static final int NETWORK_TYPE_1 = NETWORK_TYPE_WIFI;
+ private static final String NETWORK_NAME_1 = "TEST_NETWORK1";
+ private static final String HOTSPOT_SSID_1 = "TEST_SSID1";
+ private static final String HOTSPOT_BSSID_1 = "TEST _BSSID1";
+ private static final int[] HOTSPOT_SECURITY_TYPES_1 = {SECURITY_TYPE_PSK, SECURITY_TYPE_EAP};
+
+ /**
+ * Verifies parcel serialization/deserialization.
+ */
+ @Test
+ public void testParcelOperation() {
+ TetherNetwork network = buildTetherNetworkBuilder().build();
+
+ Parcel parcelW = Parcel.obtain();
+ network.writeToParcel(parcelW, 0);
+ byte[] bytes = parcelW.marshall();
+ parcelW.recycle();
+
+ Parcel parcelR = Parcel.obtain();
+ parcelR.unmarshall(bytes, 0, bytes.length);
+ parcelR.setDataPosition(0);
+ TetherNetwork fromParcel = TetherNetwork.CREATOR.createFromParcel(parcelR);
+
+ assertEquals(network, fromParcel);
+ assertEquals(network.hashCode(), fromParcel.hashCode());
+ }
+
+ /**
+ * Verifies the Equals operation
+ */
+ @Test
+ public void testEqualsOperation() {
+ TetherNetwork network1 = buildTetherNetworkBuilder().build();
+ TetherNetwork network2 = buildTetherNetworkBuilder().build();
+ assertEquals(network1, network2);
+
+ TetherNetwork.Builder builder = buildTetherNetworkBuilder().setDeviceId(DEVICE_ID_1);
+ assertNotEquals(network1, builder.build());
+
+ builder = buildTetherNetworkBuilder().setDeviceInfo(DEVICE_INFO_1);
+ assertNotEquals(network1, builder.build());
+
+ builder = buildTetherNetworkBuilder().setNetworkType(NETWORK_TYPE_1);
+ assertNotEquals(network1, builder.build());
+
+ builder = buildTetherNetworkBuilder().setNetworkName(NETWORK_NAME_1);
+ assertNotEquals(network1, builder.build());
+
+ builder = buildTetherNetworkBuilder().setHotspotSsid(HOTSPOT_SSID_1);
+ assertNotEquals(network1, builder.build());
+
+ builder = buildTetherNetworkBuilder().setHotspotBssid(HOTSPOT_BSSID_1);
+ assertNotEquals(network1, builder.build());
+
+ builder = buildTetherNetworkBuilder().setHotspotSecurityTypes(HOTSPOT_SECURITY_TYPES_1);
+ assertNotEquals(network1, builder.build());
+ }
+
+ /**
+ * Verifies the get methods return the expected data.
+ */
+ @Test
+ public void testGetMethods() {
+ TetherNetwork network = buildTetherNetworkBuilder().build();
+ assertEquals(network.getDeviceId(), DEVICE_ID);
+ assertEquals(network.getDeviceInfo(), DEVICE_INFO);
+ assertEquals(network.getNetworkType(), NETWORK_TYPE);
+ assertEquals(network.getNetworkName(), NETWORK_NAME);
+ assertEquals(network.getHotspotSsid(), HOTSPOT_SSID);
+ assertEquals(network.getHotspotBssid(), HOTSPOT_BSSID);
+ assertArrayEquals(network.getHotspotSecurityTypes(), HOTSPOT_SECURITY_TYPES);
+ }
+
+ private TetherNetwork.Builder buildTetherNetworkBuilder() {
+ return new TetherNetwork.Builder()
+ .setDeviceId(DEVICE_ID)
+ .setDeviceInfo(DEVICE_INFO)
+ .setNetworkType(NETWORK_TYPE)
+ .setNetworkName(NETWORK_NAME)
+ .setHotspotSsid(HOTSPOT_SSID)
+ .setHotspotBssid(HOTSPOT_BSSID)
+ .setHotspotSecurityTypes(HOTSPOT_SECURITY_TYPES);
+ }
+}
diff --git a/wifi/tests/src/android/net/wifi/sharedconnectivity/service/SharedConnectivityServiceTest.java b/wifi/tests/src/android/net/wifi/sharedconnectivity/service/SharedConnectivityServiceTest.java
new file mode 100644
index 0000000..e15be8b
--- /dev/null
+++ b/wifi/tests/src/android/net/wifi/sharedconnectivity/service/SharedConnectivityServiceTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.service;
+
+import static org.junit.Assert.assertNotNull;
+
+import android.content.Intent;
+import android.net.wifi.sharedconnectivity.app.KnownNetwork;
+import android.net.wifi.sharedconnectivity.app.TetherNetwork;
+import android.os.Handler;
+import android.os.test.TestLooper;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link android.net.wifi.sharedconnectivity.service.SharedConnectivityService}.
+ */
+@SmallTest
+public class SharedConnectivityServiceTest {
+
+ /**
+ * Verifies service returns
+ */
+ @Test
+ public void testOnBind() {
+ SharedConnectivityService service = createService();
+ assertNotNull(service.onBind(new Intent()));
+ }
+
+ @Test
+ public void testCallbacks() {
+ SharedConnectivityService service = createService();
+ ISharedConnectivityService.Stub binder =
+ (ISharedConnectivityService.Stub) service.onBind(new Intent());
+ }
+
+ private SharedConnectivityService createService() {
+ return new SharedConnectivityService(new Handler(new TestLooper().getLooper())) {
+ @Override
+ public void onConnectTetherNetwork(TetherNetwork network) {}
+
+ @Override
+ public void onDisconnectTetherNetwork() {}
+
+ @Override
+ public void onConnectKnownNetwork(KnownNetwork network) {}
+
+ @Override
+ public void onForgetKnownNetwork(KnownNetwork network) {}
+ };
+ }
+}