diff options
55 files changed, 1693 insertions, 614 deletions
diff --git a/api/current.txt b/api/current.txt index 4e533d1fbf95..1b1c693db312 100644 --- a/api/current.txt +++ b/api/current.txt @@ -10638,9 +10638,9 @@ package android.content { } public final class LocusId implements android.os.Parcelable { - ctor public LocusId(@NonNull android.net.Uri); + ctor public LocusId(@NonNull String); method public int describeContents(); - method @NonNull public android.net.Uri getUri(); + method @NonNull public String getId(); method public void writeToParcel(android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.content.LocusId> CREATOR; } @@ -14156,6 +14156,7 @@ package android.graphics { method @Nullable public android.graphics.ImageDecoder.OnPartialImageListener getOnPartialImageListener(); method @Nullable public android.graphics.PostProcessor getPostProcessor(); method public boolean isDecodeAsAlphaMaskEnabled(); + method public static boolean isMimeTypeSupported(@NonNull String); method public boolean isMutableRequired(); method public boolean isUnpremultipliedRequired(); method public void setAllocator(int); @@ -23052,7 +23053,7 @@ package android.media { ctor public AudioAttributes.Builder(); ctor public AudioAttributes.Builder(android.media.AudioAttributes); method public android.media.AudioAttributes build(); - method public android.media.AudioAttributes.Builder setAllowCapture(boolean); + method @NonNull public android.media.AudioAttributes.Builder setAllowCapture(boolean); method public android.media.AudioAttributes.Builder setContentType(int); method public android.media.AudioAttributes.Builder setFlags(int); method public android.media.AudioAttributes.Builder setLegacyStreamType(int); @@ -23391,11 +23392,11 @@ package android.media { public static final class AudioPlaybackCaptureConfiguration.Builder { ctor public AudioPlaybackCaptureConfiguration.Builder(@NonNull android.media.projection.MediaProjection); - method public android.media.AudioPlaybackCaptureConfiguration.Builder addMatchingUid(int); - method public android.media.AudioPlaybackCaptureConfiguration.Builder addMatchingUsage(@NonNull android.media.AudioAttributes); - method public android.media.AudioPlaybackCaptureConfiguration build(); - method public android.media.AudioPlaybackCaptureConfiguration.Builder excludeUid(int); - method public android.media.AudioPlaybackCaptureConfiguration.Builder excludeUsage(@NonNull android.media.AudioAttributes); + method @NonNull public android.media.AudioPlaybackCaptureConfiguration.Builder addMatchingUid(int); + method @NonNull public android.media.AudioPlaybackCaptureConfiguration.Builder addMatchingUsage(@NonNull android.media.AudioAttributes); + method @NonNull public android.media.AudioPlaybackCaptureConfiguration build(); + method @NonNull public android.media.AudioPlaybackCaptureConfiguration.Builder excludeUid(int); + method @NonNull public android.media.AudioPlaybackCaptureConfiguration.Builder excludeUsage(@NonNull android.media.AudioAttributes); } public final class AudioPlaybackConfiguration implements android.os.Parcelable { @@ -23496,7 +23497,7 @@ package android.media { ctor public AudioRecord.Builder(); method public android.media.AudioRecord build() throws java.lang.UnsupportedOperationException; method public android.media.AudioRecord.Builder setAudioFormat(@NonNull android.media.AudioFormat) throws java.lang.IllegalArgumentException; - method public android.media.AudioRecord.Builder setAudioPlaybackCaptureConfig(@NonNull android.media.AudioPlaybackCaptureConfiguration); + method @NonNull public android.media.AudioRecord.Builder setAudioPlaybackCaptureConfig(@NonNull android.media.AudioPlaybackCaptureConfiguration); method public android.media.AudioRecord.Builder setAudioSource(int) throws java.lang.IllegalArgumentException; method public android.media.AudioRecord.Builder setBufferSizeInBytes(int) throws java.lang.IllegalArgumentException; } @@ -38416,7 +38417,8 @@ package android.provider { method @Nullable public static android.net.Uri getDocumentUri(@NonNull android.content.Context, @NonNull android.net.Uri); method public static android.net.Uri getMediaScannerUri(); method @Nullable public static android.net.Uri getMediaUri(@NonNull android.content.Context, @NonNull android.net.Uri); - method public static String getVersion(android.content.Context); + method @NonNull public static String getVersion(@NonNull android.content.Context); + method @NonNull public static String getVersion(@NonNull android.content.Context, @NonNull String); method @NonNull public static String getVolumeName(@NonNull android.net.Uri); method @NonNull public static android.net.Uri setIncludePending(@NonNull android.net.Uri); method @NonNull public static android.net.Uri setRequireOriginal(@NonNull android.net.Uri); @@ -53034,7 +53036,7 @@ package android.view.contentcapture { public final class ContentCaptureContext implements android.os.Parcelable { method public int describeContents(); - method @NonNull public static android.view.contentcapture.ContentCaptureContext forLocusId(@NonNull android.net.Uri); + method @NonNull public static android.view.contentcapture.ContentCaptureContext forLocusId(@NonNull String); method @Nullable public android.os.Bundle getExtras(); method @NonNull public android.content.LocusId getLocusId(); method public void writeToParcel(android.os.Parcel, int); @@ -53082,18 +53084,19 @@ package android.view.contentcapture { method public boolean isForEverything(); method public void writeToParcel(android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.view.contentcapture.UserDataRemovalRequest> CREATOR; + field public static final int FLAG_IS_PREFIX = 1; // 0x1 } public static final class UserDataRemovalRequest.Builder { ctor public UserDataRemovalRequest.Builder(); - method @NonNull public android.view.contentcapture.UserDataRemovalRequest.Builder addLocusId(@NonNull android.content.LocusId, boolean); + method @NonNull public android.view.contentcapture.UserDataRemovalRequest.Builder addLocusId(@NonNull android.content.LocusId, int); method @NonNull public android.view.contentcapture.UserDataRemovalRequest build(); method @NonNull public android.view.contentcapture.UserDataRemovalRequest.Builder forEverything(); } public final class UserDataRemovalRequest.LocusIdRequest { + method @NonNull public int getFlags(); method @NonNull public android.content.LocusId getLocusId(); - method @NonNull public boolean isRecursive(); } } @@ -57329,7 +57332,7 @@ package android.widget { method @NonNull public android.view.textclassifier.TextClassifier getTextClassifier(); method public final android.content.res.ColorStateList getTextColors(); method @Nullable public android.graphics.drawable.Drawable getTextCursorDrawable(); - method public android.text.TextDirectionHeuristic getTextDirectionHeuristic(); + method @NonNull public android.text.TextDirectionHeuristic getTextDirectionHeuristic(); method @NonNull public java.util.Locale getTextLocale(); method @NonNull @Size(min=1) public android.os.LocaleList getTextLocales(); method @NonNull public android.text.PrecomputedText.Params getTextMetricsParams(); diff --git a/cmds/incident_helper/src/ih_util.cpp b/cmds/incident_helper/src/ih_util.cpp index 012310cc277a..77a56e55045b 100644 --- a/cmds/incident_helper/src/ih_util.cpp +++ b/cmds/incident_helper/src/ih_util.cpp @@ -142,7 +142,7 @@ record_t parseRecordByColumns(const std::string& line, const std::vector<int>& i } if (lineSize - lastIndex > 0) { int beginning = lastIndex; - if (record.size() == indices.size()) { + if (record.size() == indices.size() && !record.empty()) { // We've already encountered all of the columns...put whatever is // left in the last column. record.pop_back(); diff --git a/cmds/incident_helper/src/parsers/CpuInfoParser.cpp b/cmds/incident_helper/src/parsers/CpuInfoParser.cpp index 21ced9cb485c..5d525e6c7f3e 100644 --- a/cmds/incident_helper/src/parsers/CpuInfoParser.cpp +++ b/cmds/incident_helper/src/parsers/CpuInfoParser.cpp @@ -65,8 +65,9 @@ CpuInfoParser::Parse(const int in, const int out) const if (line.empty()) continue; nline++; - - if (stripPrefix(&line, "Tasks:")) { + // The format changes from time to time in toybox/toys/posix/ps.c + // With -H, it prints Threads instead of Tasks (FLAG(H)?"Thread":"Task") + if (stripPrefix(&line, "Threads:")) { writeSuffixLine(&proto, CpuInfoProto::TASK_STATS, line, COMMA_DELIMITER, CpuInfoProto::TaskStats::_FIELD_COUNT, CpuInfoProto::TaskStats::_FIELD_NAMES, diff --git a/cmds/incident_helper/testdata/cpuinfo.txt b/cmds/incident_helper/testdata/cpuinfo.txt index ec4a83960698..aa3afc33ad6a 100644 --- a/cmds/incident_helper/testdata/cpuinfo.txt +++ b/cmds/incident_helper/testdata/cpuinfo.txt @@ -1,8 +1,8 @@ -Tasks: 2038 total, 1 running,2033 sleeping, 0 stopped, 0 zombie +Threads: 2038 total, 1 running,2033 sleeping, 0 stopped, 0 zombie -Mem: 3842668k total, 3761936k used, 80732k free, 220188k buffers + Mem: 3842668k total, 3761936k used, 80732k free, 220188k buffers -Swap: 524284k total, 25892k used, 498392k free, 1316952k cached + Swap: 524284k total, 25892k used, 498392k free, 1316952k cached 400%cpu 17%user 0%nice 43%sys 338%idle 0%iow 0%irq 1%sirq 0%host @@ -12,4 +12,4 @@ Swap: 524284k total, 25892k used, 498392k free, 1316952k cached 29438 29438 rootabcdefghij 20 0 57.9 R 14M 3.8M top test top 916 916 system 18 -2 1.4 S 4.6G 404M fg system_server system_server 28 28 root -2 0 1.4 S 0 0 bg rcuc/3 [rcuc/3] - 27 27 root RT 0 1.4 S 0 0 ta migration/3 [migration/3]
\ No newline at end of file + 27 27 root RT 0 1.4 S 0 0 ta migration/3 [migration/3] diff --git a/config/hiddenapi-greylist.txt b/config/hiddenapi-greylist.txt index 46a956cab1a8..9b0e6574e5c4 100644 --- a/config/hiddenapi-greylist.txt +++ b/config/hiddenapi-greylist.txt @@ -2935,7 +2935,6 @@ Lcom/android/internal/telephony/ServiceStateTracker;->isInHomeSidNid(II)Z Lcom/android/internal/telephony/ServiceStateTracker;->isInvalidOperatorNumeric(Ljava/lang/String;)Z Lcom/android/internal/telephony/ServiceStateTracker;->log(Ljava/lang/String;)V Lcom/android/internal/telephony/ServiceStateTracker;->loge(Ljava/lang/String;)V -Lcom/android/internal/telephony/ServiceStateTracker;->mAttachedRegistrants:Landroid/os/RegistrantList; Lcom/android/internal/telephony/ServiceStateTracker;->mCi:Lcom/android/internal/telephony/CommandsInterface; Lcom/android/internal/telephony/ServiceStateTracker;->mCr:Landroid/content/ContentResolver; Lcom/android/internal/telephony/ServiceStateTracker;->mCurDataSpn:Ljava/lang/String; @@ -2947,7 +2946,6 @@ Lcom/android/internal/telephony/ServiceStateTracker;->mDataRoamingOffRegistrants Lcom/android/internal/telephony/ServiceStateTracker;->mDataRoamingOnRegistrants:Landroid/os/RegistrantList; Lcom/android/internal/telephony/ServiceStateTracker;->mDefaultRoamingIndicator:I Lcom/android/internal/telephony/ServiceStateTracker;->mDesiredPowerState:Z -Lcom/android/internal/telephony/ServiceStateTracker;->mDetachedRegistrants:Landroid/os/RegistrantList; Lcom/android/internal/telephony/ServiceStateTracker;->mDeviceShuttingDown:Z Lcom/android/internal/telephony/ServiceStateTracker;->mEmergencyOnly:Z Lcom/android/internal/telephony/ServiceStateTracker;->mIccRecords:Lcom/android/internal/telephony/uicc/IccRecords; @@ -2975,7 +2973,6 @@ Lcom/android/internal/telephony/ServiceStateTracker;->mUiccApplcation:Lcom/andro Lcom/android/internal/telephony/ServiceStateTracker;->mUiccController:Lcom/android/internal/telephony/uicc/UiccController; Lcom/android/internal/telephony/ServiceStateTracker;->mVoiceRoamingOffRegistrants:Landroid/os/RegistrantList; Lcom/android/internal/telephony/ServiceStateTracker;->mVoiceRoamingOnRegistrants:Landroid/os/RegistrantList; -Lcom/android/internal/telephony/ServiceStateTracker;->notifyDataRegStateRilRadioTechnologyChanged()V Lcom/android/internal/telephony/ServiceStateTracker;->notifySignalStrength()Z Lcom/android/internal/telephony/ServiceStateTracker;->pollState()V Lcom/android/internal/telephony/ServiceStateTracker;->reRegisterNetwork(Landroid/os/Message;)V diff --git a/core/java/android/content/LocusId.java b/core/java/android/content/LocusId.java index 2142cf3ebda3..3d1ddc3ca77b 100644 --- a/core/java/android/content/LocusId.java +++ b/core/java/android/content/LocusId.java @@ -16,7 +16,6 @@ package android.content; import android.annotation.NonNull; -import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; @@ -34,28 +33,28 @@ import java.io.PrintWriter; // TODO(b/123577059): make sure this is well documented and understandable public final class LocusId implements Parcelable { - private final Uri mUri; + private final String mId; /** * Default constructor. */ - public LocusId(@NonNull Uri uri) { - mUri = Preconditions.checkNotNull(uri); + public LocusId(@NonNull String id) { + mId = Preconditions.checkNotNull(id); } /** - * Gets the {@code uri} associated with the locus. + * Gets the {@code id} associated with the locus. */ @NonNull - public Uri getUri() { - return mUri; + public String getId() { + return mId; } @Override public int hashCode() { final int prime = 31; int result = 1; - result = prime * result + ((mUri == null) ? 0 : mUri.hashCode()); + result = prime * result + ((mId == null) ? 0 : mId.hashCode()); return result; } @@ -65,26 +64,27 @@ public final class LocusId implements Parcelable { if (obj == null) return false; if (getClass() != obj.getClass()) return false; final LocusId other = (LocusId) obj; - if (mUri == null) { - if (other.mUri != null) return false; + if (mId == null) { + if (other.mId != null) return false; } else { - if (!mUri.equals(other.mUri)) return false; + if (!mId.equals(other.mId)) return false; } return true; } @Override public String toString() { - return "LocusId[uri=" + getSanitizedUri() + "]"; + return "LocusId[" + getSanitizedId() + "]"; } /** @hide */ public void dump(@NonNull PrintWriter pw) { - pw.print("uri:"); pw.println(getSanitizedUri()); + pw.print("id:"); pw.println(getSanitizedId()); } - private String getSanitizedUri() { - final int size = mUri.toString().length(); + @NonNull + private String getSanitizedId() { + final int size = mId.length(); return size + "_chars"; } @@ -94,8 +94,8 @@ public final class LocusId implements Parcelable { } @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeParcelable(mUri, flags); + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeString(mId); } public static final @android.annotation.NonNull Parcelable.Creator<LocusId> CREATOR = @@ -103,9 +103,8 @@ public final class LocusId implements Parcelable { @NonNull @Override - public LocusId createFromParcel(Parcel source) { - final Uri uri = source.readParcelable(null); - return new LocusId(uri); + public LocusId createFromParcel(Parcel parcel) { + return new LocusId(parcel.readString()); } @NonNull diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java index 38ea43e0219b..0cc5f3931487 100644 --- a/core/java/android/content/pm/LauncherApps.java +++ b/core/java/android/content/pm/LauncherApps.java @@ -792,7 +792,7 @@ public class LauncherApps { * * @return an {@link AppUsageLimit} object describing the app time limit containing * the given package with the smallest time remaining, or {@code null} if none exist. - * @throws SecurityException when the caller is not the active launcher. + * @throws SecurityException when the caller is not the recents app. */ @Nullable public LauncherApps.AppUsageLimit getAppUsageLimit(@NonNull String packageName, diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index 0304f19cc5b4..b20cce9b7e3e 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -1343,6 +1343,7 @@ public class PackageInstaller { */ public boolean areHiddenOptionsSet() { return (installFlags & (PackageManager.INSTALL_ALLOW_DOWNGRADE + | PackageManager.INSTALL_RESPECT_ALLOW_DOWNGRADE | PackageManager.INSTALL_DONT_KILL_APP | PackageManager.INSTALL_INSTANT_APP | PackageManager.INSTALL_FULL_APP diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index a5464c2137af..c133fba031f2 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -721,6 +721,7 @@ public abstract class PackageManager { INSTALL_VIRTUAL_PRELOAD, INSTALL_APEX, INSTALL_ENABLE_ROLLBACK, + INSTALL_RESPECT_ALLOW_DOWNGRADE, }) @Retention(RetentionPolicy.SOURCE) public @interface InstallFlags {} @@ -865,6 +866,15 @@ public abstract class PackageManager { */ public static final int INSTALL_DISABLE_VERIFICATION = 0x00080000; + /** + * Flag parameter for {@link #installPackage} to indicate that + * {@link #INSTALL_ALLOW_DOWNGRADE} should be respected. + * + * @hide + */ + // TODO(b/127322579): rename + public static final int INSTALL_RESPECT_ALLOW_DOWNGRADE = 0x00100000; + /** @hide */ @IntDef(flag = true, prefix = { "DONT_KILL_APP" }, value = { DONT_KILL_APP diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java index a25bbdb8bf89..9da8e4eb0a6d 100644 --- a/core/java/android/hardware/camera2/CameraCharacteristics.java +++ b/core/java/android/hardware/camera2/CameraCharacteristics.java @@ -85,7 +85,8 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri * * @hide */ - public Key(String name, String fallbackName, Class<T> type) { + @UnsupportedAppUsage + public Key(@NonNull String name, @NonNull String fallbackName, @NonNull Class<T> type) { mKey = new CameraMetadataNative.Key<T>(name, fallbackName, type); } diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java index 53d4dd3d36ee..bb0987dd0798 100644 --- a/core/java/android/hardware/camera2/CaptureResult.java +++ b/core/java/android/hardware/camera2/CaptureResult.java @@ -89,7 +89,8 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * * @hide */ - public Key(String name, String fallbackName, Class<T> type) { + @UnsupportedAppUsage + public Key(@NonNull String name, @NonNull String fallbackName, @NonNull Class<T> type) { mKey = new CameraMetadataNative.Key<T>(name, fallbackName, type); } @@ -4251,6 +4252,7 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * @see CaptureResult#SENSOR_TIMESTAMP * @hide */ + @UnsupportedAppUsage public static final Key<long[]> STATISTICS_OIS_TIMESTAMPS = new Key<long[]>("android.statistics.oisTimestamps", long[].class); @@ -4270,6 +4272,7 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p> * @hide */ + @UnsupportedAppUsage public static final Key<float[]> STATISTICS_OIS_X_SHIFTS = new Key<float[]>("android.statistics.oisXShifts", float[].class); @@ -4289,6 +4292,7 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p> * @hide */ + @UnsupportedAppUsage public static final Key<float[]> STATISTICS_OIS_Y_SHIFTS = new Key<float[]>("android.statistics.oisYShifts", float[].class); diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java index a34ac706c406..917b5c2615db 100644 --- a/core/java/android/provider/MediaStore.java +++ b/core/java/android/provider/MediaStore.java @@ -146,6 +146,8 @@ public final class MediaStore { public static final String RETRANSLATE_CALL = "update_titles"; /** {@hide} */ + public static final String GET_VERSION_CALL = "get_version"; + /** {@hide} */ public static final String GET_DOCUMENT_URI_CALL = "get_document_uri"; /** {@hide} */ public static final String GET_MEDIA_URI_CALL = "get_media_uri"; @@ -3318,21 +3320,41 @@ public final class MediaStore { public static final String MEDIA_IGNORE_FILENAME = ".nomedia"; /** - * Get the media provider's version. - * Applications that import data from the media provider into their own caches - * can use this to detect that the media provider changed, and reimport data - * as needed. No other assumptions should be made about the meaning of the version. - * @param context Context to use for performing the query. - * @return A version string, or null if the version could not be determined. + * Return an opaque version string describing the {@link MediaStore} state. + * <p> + * Applications that import data from {@link MediaStore} into their own + * caches can use this to detect that {@link MediaStore} has undergone + * substantial changes, and that data should be rescanned. + * <p> + * No other assumptions should be made about the meaning of the version. + * <p> + * This method returns the version for {@link MediaStore#VOLUME_EXTERNAL}; + * to obtain a version for a different volume, use + * {@link #getVersion(Context, String)}. */ - public static String getVersion(Context context) { - final Uri uri = AUTHORITY_URI.buildUpon().appendPath("none").appendPath("version").build(); - try (Cursor c = context.getContentResolver().query(uri, null, null, null, null)) { - if (c.moveToFirst()) { - return c.getString(0); - } + public static @NonNull String getVersion(@NonNull Context context) { + return getVersion(context, VOLUME_EXTERNAL); + } + + /** + * Return an opaque version string describing the {@link MediaStore} state. + * <p> + * Applications that import data from {@link MediaStore} into their own + * caches can use this to detect that {@link MediaStore} has undergone + * substantial changes, and that data should be rescanned. + * <p> + * No other assumptions should be made about the meaning of the version. + */ + public static @NonNull String getVersion(@NonNull Context context, @NonNull String volumeName) { + final ContentResolver resolver = context.getContentResolver(); + try (ContentProviderClient client = resolver.acquireContentProviderClient(AUTHORITY)) { + final Bundle in = new Bundle(); + in.putString(Intent.EXTRA_TEXT, volumeName); + final Bundle out = client.call(GET_VERSION_CALL, null, in); + return out.getString(Intent.EXTRA_TEXT); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); } - return null; } /** diff --git a/core/java/android/view/TEST_MAPPING b/core/java/android/view/TEST_MAPPING new file mode 100644 index 000000000000..87d428ab551e --- /dev/null +++ b/core/java/android/view/TEST_MAPPING @@ -0,0 +1,10 @@ +{ + "presubmit": [ + { + "name": "CtsUiRenderingTestCases" + }, + { + "name": "CtsAccelerationTestCases" + } + ] +}
\ No newline at end of file diff --git a/core/java/android/view/contentcapture/ContentCaptureContext.java b/core/java/android/view/contentcapture/ContentCaptureContext.java index 86f85bfecc85..b9dc0dd99a1d 100644 --- a/core/java/android/view/contentcapture/ContentCaptureContext.java +++ b/core/java/android/view/contentcapture/ContentCaptureContext.java @@ -24,7 +24,6 @@ import android.app.TaskInfo; import android.content.ComponentName; import android.content.Context; import android.content.LocusId; -import android.net.Uri; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; @@ -212,11 +211,11 @@ public final class ContentCaptureContext implements Parcelable { } /** - * Helper that creates a {@link ContentCaptureContext} associated with the given {@code uri}. + * Helper that creates a {@link ContentCaptureContext} associated with the given {@code id}. */ @NonNull - public static ContentCaptureContext forLocusId(@NonNull Uri uri) { - return new Builder(new LocusId(uri)).build(); + public static ContentCaptureContext forLocusId(@NonNull String id) { + return new Builder(new LocusId(id)).build(); } /** diff --git a/core/java/android/view/contentcapture/UserDataRemovalRequest.java b/core/java/android/view/contentcapture/UserDataRemovalRequest.java index b273f7c15c01..3e1e4abaa84c 100644 --- a/core/java/android/view/contentcapture/UserDataRemovalRequest.java +++ b/core/java/android/view/contentcapture/UserDataRemovalRequest.java @@ -15,6 +15,7 @@ */ package android.view.contentcapture; +import android.annotation.IntDef; import android.annotation.NonNull; import android.app.ActivityThread; import android.content.LocusId; @@ -24,6 +25,8 @@ import android.util.IntArray; import com.android.internal.util.Preconditions; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; @@ -33,6 +36,19 @@ import java.util.List; */ public final class UserDataRemovalRequest implements Parcelable { + /** + * When set, service should use the {@link LocusId#getId()} as prefix for the data to be + * removed. + */ + public static final int FLAG_IS_PREFIX = 0x1; + + /** @hide */ + @IntDef(prefix = { "FLAG" }, flag = true, value = { + FLAG_IS_PREFIX + }) + @Retention(RetentionPolicy.SOURCE) + @interface Flags {} + private final String mPackageName; private final boolean mForEverything; @@ -46,7 +62,7 @@ public final class UserDataRemovalRequest implements Parcelable { mLocusIdRequests = new ArrayList<>(size); for (int i = 0; i < size; i++) { mLocusIdRequests.add(new LocusIdRequest(builder.mLocusIds.get(i), - builder.mRecursive.get(i) == 1)); + builder.mFlags.get(i))); } } } @@ -59,7 +75,7 @@ public final class UserDataRemovalRequest implements Parcelable { mLocusIdRequests = new ArrayList<>(size); for (int i = 0; i < size; i++) { mLocusIdRequests.add(new LocusIdRequest((LocusId) parcel.readValue(null), - parcel.readBoolean())); + parcel.readInt())); } } } @@ -94,7 +110,7 @@ public final class UserDataRemovalRequest implements Parcelable { private boolean mForEverything; private ArrayList<LocusId> mLocusIds; - private IntArray mRecursive; + private IntArray mFlags; private boolean mDestroyed; @@ -116,24 +132,24 @@ public final class UserDataRemovalRequest implements Parcelable { * Request service to remove data associated with a given {@link LocusId}. * * @param locusId the {@link LocusId} being requested to be removed. - * @param recursive whether it should remove the data associated with just the - * {@code LocusId} or its tree of descendants. + * @param flags either {@link UserDataRemovalRequest#FLAG_IS_PREFIX} or {@code 0} * * @return this builder */ @NonNull - public Builder addLocusId(@NonNull LocusId locusId, boolean recursive) { + public Builder addLocusId(@NonNull LocusId locusId, @Flags int flags) { throwIfDestroyed(); Preconditions.checkState(!mForEverything, "Already is for everything"); Preconditions.checkNotNull(locusId); + // felipeal: check flags if (mLocusIds == null) { mLocusIds = new ArrayList<>(); - mRecursive = new IntArray(); + mFlags = new IntArray(); } mLocusIds.add(locusId); - mRecursive.add(recursive ? 1 : 0); + mFlags.add(flags); return this; } @@ -144,7 +160,8 @@ public final class UserDataRemovalRequest implements Parcelable { public UserDataRemovalRequest build() { throwIfDestroyed(); - Preconditions.checkState(mForEverything || mLocusIds != null); + Preconditions.checkState(mForEverything || mLocusIds != null, + "must call either #forEverything() or add one #addLocusId()"); mDestroyed = true; return new UserDataRemovalRequest(this); @@ -170,7 +187,7 @@ public final class UserDataRemovalRequest implements Parcelable { for (int i = 0; i < size; i++) { final LocusIdRequest request = mLocusIdRequests.get(i); parcel.writeValue(request.getLocusId()); - parcel.writeBoolean(request.isRecursive()); + parcel.writeInt(request.getFlags()); } } } @@ -196,11 +213,11 @@ public final class UserDataRemovalRequest implements Parcelable { */ public final class LocusIdRequest { private final @NonNull LocusId mLocusId; - private final boolean mRecursive; + private final @Flags int mFlags; - private LocusIdRequest(@NonNull LocusId locusId, boolean recursive) { + private LocusIdRequest(@NonNull LocusId locusId, @Flags int flags) { this.mLocusId = locusId; - this.mRecursive = recursive; + this.mFlags = flags; } /** @@ -212,12 +229,13 @@ public final class UserDataRemovalRequest implements Parcelable { } /** - * Checks whether the request is to remove just the data associated with the {@link LocusId} - * per se, or also its descendants. + * Gets the flags associates with request. + * + * @return either {@link UserDataRemovalRequest#FLAG_IS_PREFIX} or {@code 0}. */ @NonNull - public boolean isRecursive() { - return mRecursive; + public @Flags int getFlags() { + return mFlags; } } } diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 73792b08194b..04bcb1451b61 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -12744,7 +12744,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * return value may not be the same as the one TextView uses if the View's layout direction is * not resolved or detached from parent root view. */ - public TextDirectionHeuristic getTextDirectionHeuristic() { + public @NonNull TextDirectionHeuristic getTextDirectionHeuristic() { if (hasPasswordTransformationMethod()) { // passwords fields should be LTR return TextDirectionHeuristics.LTR; diff --git a/core/tests/coretests/src/android/view/contentcapture/ContentCaptureEventTest.java b/core/tests/coretests/src/android/view/contentcapture/ContentCaptureEventTest.java index a5ac2707b620..de2edc3637e4 100644 --- a/core/tests/coretests/src/android/view/contentcapture/ContentCaptureEventTest.java +++ b/core/tests/coretests/src/android/view/contentcapture/ContentCaptureEventTest.java @@ -25,7 +25,6 @@ import static com.google.common.truth.Truth.assertThat; import static org.testng.Assert.assertThrows; import android.content.LocusId; -import android.net.Uri; import android.os.Parcel; import android.os.SystemClock; import android.view.autofill.AutofillId; @@ -47,7 +46,7 @@ public class ContentCaptureEventTest { private static final long MY_EPOCH = SystemClock.uptimeMillis(); - private static final LocusId ID = new LocusId(Uri.parse("WHATEVER")); + private static final LocusId ID = new LocusId("WHATEVER"); // Not using @Mock because it's final - no need to be fancy here.... private final ContentCaptureContext mClientContext = diff --git a/graphics/java/android/graphics/ImageDecoder.java b/graphics/java/android/graphics/ImageDecoder.java index 7016cc741e90..2cf802bb9631 100644 --- a/graphics/java/android/graphics/ImageDecoder.java +++ b/graphics/java/android/graphics/ImageDecoder.java @@ -59,6 +59,8 @@ import java.io.IOException; import java.io.InputStream; import java.lang.annotation.Retention; import java.nio.ByteBuffer; +import java.util.Locale; +import java.util.Objects; import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicBoolean; @@ -838,6 +840,40 @@ public final class ImageDecoder implements AutoCloseable { } /** + * Return if the given MIME type is a supported file format that can be + * decoded by this class. This can be useful to determine if a file can be + * decoded directly, or if it needs to be converted into a more general + * format using an API like {@link ContentResolver#openTypedAssetFile}. + */ + public static boolean isMimeTypeSupported(@NonNull String mimeType) { + Objects.requireNonNull(mimeType); + switch (mimeType.toLowerCase(Locale.US)) { + case "image/png": + case "image/jpeg": + case "image/webp": + case "image/gif": + case "image/heif": + case "image/heic": + case "image/bmp": + case "image/x-ico": + case "image/vnd.wap.wbmp": + case "image/x-sony-arw": + case "image/x-canon-cr2": + case "image/x-adobe-dng": + case "image/x-nikon-nef": + case "image/x-nikon-nrw": + case "image/x-olympus-orf": + case "image/x-fuji-raf": + case "image/x-panasonic-rw2": + case "image/x-pentax-pef": + case "image/x-samsung-srw": + return true; + default: + return false; + } + } + + /** * Create a new {@link Source Source} from a resource. * * @param res the {@link Resources} object containing the image data. diff --git a/libs/hwui/TEST_MAPPING b/libs/hwui/TEST_MAPPING new file mode 100644 index 000000000000..d9f2acbb49d2 --- /dev/null +++ b/libs/hwui/TEST_MAPPING @@ -0,0 +1,13 @@ +{ + "presubmit": [ + { + "name": "CtsUiRenderingTestCases" + }, + { + "name": "CtsGraphicsTestCases" + }, + { + "name": "CtsAccelerationTestCases" + } + ] +}
\ No newline at end of file diff --git a/media/java/android/media/AudioAttributes.java b/media/java/android/media/AudioAttributes.java index 46d4204b4d5e..aed8e4ede1c4 100644 --- a/media/java/android/media/AudioAttributes.java +++ b/media/java/android/media/AudioAttributes.java @@ -630,7 +630,7 @@ public final class AudioAttributes implements Parcelable { * true to allow apps to capture the audio * @return the same Builder instance */ - public Builder setAllowCapture(boolean allowCapture) { + public @NonNull Builder setAllowCapture(boolean allowCapture) { if (allowCapture) { mFlags &= ~FLAG_NO_CAPTURE; } else { diff --git a/media/java/android/media/AudioPlaybackCaptureConfiguration.java b/media/java/android/media/AudioPlaybackCaptureConfiguration.java index 9a16aea1e052..333cd2d4f0cf 100644 --- a/media/java/android/media/AudioPlaybackCaptureConfiguration.java +++ b/media/java/android/media/AudioPlaybackCaptureConfiguration.java @@ -72,7 +72,7 @@ public final class AudioPlaybackCaptureConfiguration { * * @param audioFormat The format in which to capture the audio. */ - AudioMix createAudioMix(AudioFormat audioFormat) { + @NonNull AudioMix createAudioMix(@NonNull AudioFormat audioFormat) { return new AudioMix.Builder(mAudioMixingRule) .setFormat(audioFormat) .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK | AudioMix.ROUTE_FLAG_RENDER) @@ -123,7 +123,7 @@ public final class AudioPlaybackCaptureConfiguration { * @throws IllegalStateException if called in conjunction with * {@link #excludeUsage(AudioAttributes)}. */ - public Builder addMatchingUsage(@NonNull AudioAttributes audioAttributes) { + public @NonNull Builder addMatchingUsage(@NonNull AudioAttributes audioAttributes) { Preconditions.checkNotNull(audioAttributes); Preconditions.checkState( mUsageMatchType != MATCH_TYPE_EXCLUSIVE, ERROR_MESSAGE_MISMATCHED_RULES); @@ -141,7 +141,7 @@ public final class AudioPlaybackCaptureConfiguration { * * @throws IllegalStateException if called in conjunction with {@link #excludeUid(int)}. */ - public Builder addMatchingUid(int uid) { + public @NonNull Builder addMatchingUid(int uid) { Preconditions.checkState( mUidMatchType != MATCH_TYPE_EXCLUSIVE, ERROR_MESSAGE_MISMATCHED_RULES); mAudioMixingRuleBuilder.addMixRule(AudioMixingRule.RULE_MATCH_UID, uid); @@ -158,7 +158,7 @@ public final class AudioPlaybackCaptureConfiguration { * @throws IllegalStateException if called in conjunction with * {@link #addMatchingUsage(AudioAttributes)}. */ - public Builder excludeUsage(@NonNull AudioAttributes audioAttributes) { + public @NonNull Builder excludeUsage(@NonNull AudioAttributes audioAttributes) { Preconditions.checkNotNull(audioAttributes); Preconditions.checkState( mUsageMatchType != MATCH_TYPE_INCLUSIVE, ERROR_MESSAGE_MISMATCHED_RULES); @@ -176,7 +176,7 @@ public final class AudioPlaybackCaptureConfiguration { * * @throws IllegalStateException if called in conjunction with {@link #addMatchingUid(int)}. */ - public Builder excludeUid(int uid) { + public @NonNull Builder excludeUid(int uid) { Preconditions.checkState( mUidMatchType != MATCH_TYPE_INCLUSIVE, ERROR_MESSAGE_MISMATCHED_RULES); mAudioMixingRuleBuilder.excludeMixRule(AudioMixingRule.RULE_MATCH_UID, uid); @@ -189,7 +189,7 @@ public final class AudioPlaybackCaptureConfiguration { * * @throws UnsupportedOperationException if the parameters set are incompatible. */ - public AudioPlaybackCaptureConfiguration build() { + public @NonNull AudioPlaybackCaptureConfiguration build() { return new AudioPlaybackCaptureConfiguration(mAudioMixingRuleBuilder.build(), mProjection); } diff --git a/media/java/android/media/AudioRecord.java b/media/java/android/media/AudioRecord.java index 3d5120f86db3..28937a65ad0d 100644 --- a/media/java/android/media/AudioRecord.java +++ b/media/java/android/media/AudioRecord.java @@ -618,7 +618,7 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection, * @throws IllegalStateException if called in conjunction with {@link #setAudioSource(int)}. * @throws NullPointerException if {@code config} is null. */ - public Builder setAudioPlaybackCaptureConfig( + public @NonNull Builder setAudioPlaybackCaptureConfig( @NonNull AudioPlaybackCaptureConfiguration config) { Preconditions.checkNotNull( config, "Illegal null AudioPlaybackCaptureConfiguration argument"); @@ -647,7 +647,7 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection, return this; } - private AudioRecord buildAudioPlaybackCaptureRecord() { + private @NonNull AudioRecord buildAudioPlaybackCaptureRecord() { AudioMix audioMix = mAudioPlaybackCaptureConfiguration.createAudioMix(mFormat); MediaProjection projection = mAudioPlaybackCaptureConfiguration.getMediaProjection(); AudioPolicy audioPolicy = new AudioPolicy.Builder(/*context=*/ null) diff --git a/packages/SystemUI/res/raw/image_wallpaper_fragment_shader.glsl b/packages/SystemUI/res/raw/image_wallpaper_fragment_shader.glsl index 586cdf3fbaae..716e1272f871 100644 --- a/packages/SystemUI/res/raw/image_wallpaper_fragment_shader.glsl +++ b/packages/SystemUI/res/raw/image_wallpaper_fragment_shader.glsl @@ -1,27 +1,76 @@ precision mediump float; +// The actual wallpaper texture. uniform sampler2D uTexture; -uniform float uCenterReveal; + +// The 85th percenile for the luminance histogram of the image (a value between 0 and 1). +// This value represents the point in histogram that includes 85% of the pixels of the image. +uniform float uPer85; + +// Reveal is the animation value that goes from 1 (the image is hidden) to 0 (the image is visible). uniform float uReveal; + +// The opacity of locked screen (constant value). uniform float uAod2Opacity; varying vec2 vTextureCoordinates; +/* + * Calculates the relative luminance of the pixel. + */ vec3 luminosity(vec3 color) { float lum = 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b; return vec3(lum); } vec4 transform(vec3 diffuse) { - // TODO: Add well comments here, tracking on b/123615467. + // Getting the luminance for this pixel vec3 lum = luminosity(diffuse); - diffuse = mix(diffuse, lum, smoothstep(0., uCenterReveal, uReveal)); - float val = mix(uReveal, uCenterReveal, step(uCenterReveal, uReveal)); - diffuse = smoothstep(val, 1.0, diffuse); - diffuse *= uAod2Opacity * (1. - smoothstep(uCenterReveal, 1., uReveal)); + + /* + * while the reveal > per85, it shows the luminance image (B&W image) + * then when moving passed that value, the image gets colored. + */ + float trans = smoothstep(0., uPer85, uReveal); + diffuse = mix(diffuse, lum, trans); + + // 'lower' value represents the capped 'reveal' value to the range [0, per85] + float selector = step(uPer85, uReveal); + float lower = mix(uReveal, uPer85, selector); + + /* + * Remaps image: + * - from reveal=1 to reveal=per85 => lower=per85, diffuse=luminance + * That means that remaps black and white image pixel + * from a possible values of [0,1] to [per85, 1] (if the pixel is darker than per85, + * it's gonna be black, if it's between per85 and 1, it's gonna be gray + * and if it's 1 it's gonna be white). + * - from reveal=per85 to reveal=0 => lower=reveal, 'diffuse' changes from luminance to color + * That means that remaps each image pixel color (rgb) + * from a possible values of [0,1] to [lower, 1] (if the pixel color is darker than 'lower', + * it's gonna be 0, if it's between 'lower' and 1, it's gonna be remap to a value + * between 0 and 1 and if it's 1 it's gonna be 1). + * - if reveal=0 => lower=0, diffuse=color image + * The image is shown as it is, colored. + */ + vec3 remaps = smoothstep(lower, 1., diffuse); + + // Interpolate between diffuse and remaps using reveal to avoid over saturation. + diffuse = mix(diffuse, remaps, uReveal); + + /* + * Fades in the pixel value: + * - if reveal=1 => fadeInOpacity=0 + * - from reveal=1 to reveal=per85 => 0<=fadeInOpacity<=1 + * - if reveal>per85 => fadeInOpacity=1 + */ + float fadeInOpacity = 1. - smoothstep(uPer85, 1., uReveal); + diffuse *= uAod2Opacity * fadeInOpacity; + return vec4(diffuse.r, diffuse.g, diffuse.b, 1.); } void main() { + // gets the pixel value of the wallpaper for this uv coordinates on screen. vec4 fragColor = texture2D(uTexture, vTextureCoordinates); gl_FragColor = transform(fragColor.rgb); }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java index 507c82246293..d40fa661192c 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java @@ -1,6 +1,7 @@ package com.android.keyguard; import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ValueAnimator; import android.app.WallpaperManager; @@ -31,6 +32,9 @@ import com.android.systemui.plugins.ClockPlugin; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.StatusBarState; +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.Arrays; import java.util.TimeZone; /** @@ -333,6 +337,19 @@ public class KeyguardClockSwitch extends RelativeLayout { return mStateListener; } + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("KeyguardClockSwitch:"); + pw.println(" mClockPlugin: " + mClockPlugin); + pw.println(" mClockView: " + mClockView); + pw.println(" mSmallClockFrame: " + mSmallClockFrame); + pw.println(" mBigClockContainer: " + mBigClockContainer); + pw.println(" mKeyguardStatusArea: " + mKeyguardStatusArea); + pw.println(" mDarkAmount: " + mDarkAmount); + pw.println(" mShowingHeader: " + mShowingHeader); + pw.println(" mSupportsDarkText: " + mSupportsDarkText); + pw.println(" mColorPalette: " + Arrays.toString(mColorPalette)); + } + /** * Special layout transition that scales the clock view as its bounds change, to make it look * like the text is shrinking. @@ -372,11 +389,23 @@ public class KeyguardClockSwitch extends RelativeLayout { boundsAnimator.addUpdateListener(animation -> { float scale = MathUtils.lerp(startScale, 1f /* stop */, animation.getAnimatedFraction()); - mClockView.setPivotX(mClockView.getWidth() / 2); + mClockView.setPivotX(mClockView.getWidth() / 2f); mClockView.setPivotY(0); mClockView.setScaleX(scale); mClockView.setScaleY(scale); }); + boundsAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animator) { + mClockView.setScaleX(1f); + mClockView.setScaleY(1f); + } + + @Override + public void onAnimationCancel(Animator animator) { + onAnimationEnd(animator); + } + }); } return animator; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java index 2040a76b61d3..8ebe1ae80d26 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java @@ -64,6 +64,8 @@ import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.tuner.TunerService; import com.android.systemui.util.wakelock.KeepAwakeAnimationListener; +import java.io.FileDescriptor; +import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -174,6 +176,7 @@ public class KeyguardSliceView extends LinearLayout implements View.OnClickListe if (mContentChangeListener != null) { mContentChangeListener.run(); } + Trace.endSection(); return; } @@ -375,6 +378,17 @@ public class KeyguardSliceView extends LinearLayout implements View.OnClickListe Trace.endSection(); } + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("KeyguardSliceView:"); + pw.println(" mClickActions: " + mClickActions); + pw.println(" mTitle: " + (mTitle == null ? "null" : mTitle.getVisibility() == VISIBLE)); + pw.println(" mRow: " + (mRow == null ? "null" : mRow.getVisibility() == VISIBLE)); + pw.println(" mTextColor: " + Integer.toHexString(mTextColor)); + pw.println(" mDarkAmount: " + mDarkAmount); + pw.println(" mSlice: " + mSlice); + pw.println(" mHasHeader: " + mHasHeader); + } + public static class Row extends LinearLayout { /** diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java index 17546c512778..808e264258da 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java @@ -41,6 +41,8 @@ import com.android.internal.widget.LockPatternUtils; import com.android.systemui.Dependency; import com.android.systemui.statusbar.policy.ConfigurationController; +import java.io.FileDescriptor; +import java.io.PrintWriter; import java.util.Locale; import java.util.TimeZone; @@ -289,6 +291,24 @@ public class KeyguardStatusView extends GridLayout implements return false; } + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("KeyguardStatusView:"); + pw.println(" mOwnerInfo: " + (mOwnerInfo == null + ? "null" : mOwnerInfo.getVisibility() == VISIBLE)); + pw.println(" mPulsing: " + mPulsing); + pw.println(" mDarkAmount: " + mDarkAmount); + pw.println(" mTextColor: " + Integer.toHexString(mTextColor)); + if (mLogoutView != null) { + pw.println(" logout visible: " + (mLogoutView.getVisibility() == VISIBLE)); + } + if (mClockView != null) { + mClockView.dump(fd, pw, args); + } + if (mKeyguardSlice != null) { + mKeyguardSlice.dump(fd, pw, args); + } + } + // DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often. // This is an optimization to ensure we only recompute the patterns when the inputs change. private static final class Patterns { diff --git a/packages/SystemUI/src/com/android/systemui/glwallpaper/ImageGLWallpaper.java b/packages/SystemUI/src/com/android/systemui/glwallpaper/ImageGLWallpaper.java index 19d85b155cba..a313336e3d71 100644 --- a/packages/SystemUI/src/com/android/systemui/glwallpaper/ImageGLWallpaper.java +++ b/packages/SystemUI/src/com/android/systemui/glwallpaper/ImageGLWallpaper.java @@ -50,7 +50,7 @@ class ImageGLWallpaper { static final String A_POSITION = "aPosition"; static final String A_TEXTURE_COORDINATES = "aTextureCoordinates"; - static final String U_CENTER_REVEAL = "uCenterReveal"; + static final String U_PER85 = "uPer85"; static final String U_REVEAL = "uReveal"; static final String U_AOD2OPACITY = "uAod2Opacity"; static final String U_TEXTURE = "uTexture"; @@ -87,7 +87,7 @@ class ImageGLWallpaper { private int mAttrPosition; private int mAttrTextureCoordinates; private int mUniAod2Opacity; - private int mUniCenterReveal; + private int mUniPer85; private int mUniReveal; private int mUniTexture; private int mTextureId; @@ -131,7 +131,7 @@ class ImageGLWallpaper { private void setupUniforms() { mUniAod2Opacity = mProgram.getUniformHandle(U_AOD2OPACITY); - mUniCenterReveal = mProgram.getUniformHandle(U_CENTER_REVEAL); + mUniPer85 = mProgram.getUniformHandle(U_PER85); mUniReveal = mProgram.getUniformHandle(U_REVEAL); mUniTexture = mProgram.getUniformHandle(U_TEXTURE); } @@ -144,8 +144,8 @@ class ImageGLWallpaper { return mAttrTextureCoordinates; case U_AOD2OPACITY: return mUniAod2Opacity; - case U_CENTER_REVEAL: - return mUniCenterReveal; + case U_PER85: + return mUniPer85; case U_REVEAL: return mUniReveal; case U_TEXTURE: diff --git a/packages/SystemUI/src/com/android/systemui/glwallpaper/ImageWallpaperRenderer.java b/packages/SystemUI/src/com/android/systemui/glwallpaper/ImageWallpaperRenderer.java index 991b1161dde2..72950088eb39 100644 --- a/packages/SystemUI/src/com/android/systemui/glwallpaper/ImageWallpaperRenderer.java +++ b/packages/SystemUI/src/com/android/systemui/glwallpaper/ImageWallpaperRenderer.java @@ -93,13 +93,13 @@ public class ImageWallpaperRenderer implements GLSurfaceView.Renderer, @Override public void onDrawFrame(GL10 gl) { - float threshold = mImageProcessHelper.getPercentile85(); + float per85 = mImageProcessHelper.getPercentile85(); float reveal = mImageRevealHelper.getReveal(); glClear(GL_COLOR_BUFFER_BIT); glUniform1f(mWallpaper.getHandle(ImageGLWallpaper.U_AOD2OPACITY), 1); - glUniform1f(mWallpaper.getHandle(ImageGLWallpaper.U_CENTER_REVEAL), threshold); + glUniform1f(mWallpaper.getHandle(ImageGLWallpaper.U_PER85), per85); glUniform1f(mWallpaper.getHandle(ImageGLWallpaper.U_REVEAL), reveal); mWallpaper.useTexture(); diff --git a/packages/SystemUI/src/com/android/systemui/power/BatteryStateSnapshot.kt b/packages/SystemUI/src/com/android/systemui/power/BatteryStateSnapshot.kt new file mode 100644 index 000000000000..d7a2d9acf3b5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/power/BatteryStateSnapshot.kt @@ -0,0 +1,55 @@ +package com.android.systemui.power + +import com.android.systemui.power.PowerUI.NO_ESTIMATE_AVAILABLE + +/** + * A simple data class to snapshot battery state when a particular check for the + * low battery warning is running in the background. + */ +data class BatteryStateSnapshot( + val batteryLevel: Int, + val isPowerSaver: Boolean, + val plugged: Boolean, + val bucket: Int, + val batteryStatus: Int, + val severeLevelThreshold: Int, + val lowLevelThreshold: Int, + val timeRemainingMillis: Long, + val severeThresholdMillis: Long, + val lowThresholdMillis: Long, + val isBasedOnUsage: Boolean +) { + /** + * Returns whether hybrid warning logic/copy should be used for this snapshot + */ + var isHybrid: Boolean = false + private set + + init { + this.isHybrid = true + } + + constructor( + batteryLevel: Int, + isPowerSaver: Boolean, + plugged: Boolean, + bucket: Int, + batteryStatus: Int, + severeLevelThreshold: Int, + lowLevelThreshold: Int + ) : this( + batteryLevel, + isPowerSaver, + plugged, + bucket, + batteryStatus, + severeLevelThreshold, + lowLevelThreshold, + NO_ESTIMATE_AVAILABLE.toLong(), + NO_ESTIMATE_AVAILABLE.toLong(), + NO_ESTIMATE_AVAILABLE.toLong(), + false + ) { + this.isHybrid = false + } +} diff --git a/packages/SystemUI/src/com/android/systemui/power/Estimate.java b/packages/SystemUI/src/com/android/systemui/power/Estimate.java deleted file mode 100644 index 12a8f0a435b4..000000000000 --- a/packages/SystemUI/src/com/android/systemui/power/Estimate.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.android.systemui.power; - -public class Estimate { - public final long estimateMillis; - public final boolean isBasedOnUsage; - - public Estimate(long estimateMillis, boolean isBasedOnUsage) { - this.estimateMillis = estimateMillis; - this.isBasedOnUsage = isBasedOnUsage; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/power/Estimate.kt b/packages/SystemUI/src/com/android/systemui/power/Estimate.kt new file mode 100644 index 000000000000..dca0d45c1c9f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/power/Estimate.kt @@ -0,0 +1,3 @@ +package com.android.systemui.power + +data class Estimate(val estimateMillis: Long, val isBasedOnUsage: Boolean)
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java index fdb0b36ee51e..41bcab53f8e9 100644 --- a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java +++ b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java @@ -134,10 +134,6 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { private int mShowing; private long mWarningTriggerTimeMs; - - private Estimate mEstimate; - private long mLowWarningThreshold; - private long mSevereWarningThreshold; private boolean mWarning; private boolean mShowAutoSaverSuggestion; private boolean mPlaySound; @@ -148,6 +144,7 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { private SystemUIDialog mHighTempDialog; private SystemUIDialog mThermalShutdownDialog; @VisibleForTesting SystemUIDialog mUsbHighTempDialog; + private BatteryStateSnapshot mCurrentBatterySnapshot; /** */ @@ -195,17 +192,8 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { } @Override - public void updateEstimate(Estimate estimate) { - mEstimate = estimate; - if (estimate.estimateMillis <= mLowWarningThreshold) { - mWarningTriggerTimeMs = System.currentTimeMillis(); - } - } - - @Override - public void updateThresholds(long lowThreshold, long severeThreshold) { - mLowWarningThreshold = lowThreshold; - mSevereWarningThreshold = severeThreshold; + public void updateSnapshot(BatteryStateSnapshot snapshot) { + mCurrentBatterySnapshot = snapshot; } private void updateNotification() { @@ -254,15 +242,17 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { protected void showWarningNotification() { final String percentage = NumberFormat.getPercentInstance() - .format((double) mBatteryLevel / 100.0); + .format((double) mCurrentBatterySnapshot.getBatteryLevel() / 100.0); - // get standard notification copy + // get shared standard notification copy String title = mContext.getString(R.string.battery_low_title); - String contentText = mContext.getString(R.string.battery_low_percent_format, percentage); + String contentText; - // override notification copy if hybrid notification enabled - if (mEstimate != null) { + // get correct content text if notification is hybrid or not + if (mCurrentBatterySnapshot.isHybrid()) { contentText = getHybridContentString(percentage); + } else { + contentText = mContext.getString(R.string.battery_low_percent_format, percentage); } final Notification.Builder nb = @@ -282,8 +272,9 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { } // Make the notification red if the percentage goes below a certain amount or the time // remaining estimate is disabled - if (mEstimate == null || mBucket < 0 - || mEstimate.estimateMillis < mSevereWarningThreshold) { + if (!mCurrentBatterySnapshot.isHybrid() || mBucket < 0 + || mCurrentBatterySnapshot.getTimeRemainingMillis() + < mCurrentBatterySnapshot.getSevereThresholdMillis()) { nb.setColor(Utils.getColorAttrDefaultColor(mContext, android.R.attr.colorError)); } @@ -324,10 +315,10 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { private String getHybridContentString(String percentage) { return PowerUtil.getBatteryRemainingStringFormatted( - mContext, - mEstimate.estimateMillis, - percentage, - mEstimate.isBasedOnUsage); + mContext, + mCurrentBatterySnapshot.getTimeRemainingMillis(), + percentage, + mCurrentBatterySnapshot.isBasedOnUsage()); } private PendingIntent pendingBroadcast(String action) { diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java index e27c25efd88f..18638606a251 100644 --- a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java +++ b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java @@ -55,6 +55,7 @@ import java.util.Arrays; import java.util.concurrent.Future; public class PowerUI extends SystemUI { + static final String TAG = "PowerUI"; static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final long TEMPERATURE_INTERVAL = 30 * DateUtils.SECOND_IN_MILLIS; @@ -63,6 +64,7 @@ public class PowerUI extends SystemUI { static final long THREE_HOURS_IN_MILLIS = DateUtils.HOUR_IN_MILLIS * 3; private static final int CHARGE_CYCLE_PERCENT_RESET = 45; private static final long SIX_HOURS_MILLIS = Duration.ofHours(6).toMillis(); + public static final int NO_ESTIMATE_AVAILABLE = -1; private final Handler mHandler = new Handler(); @VisibleForTesting @@ -71,13 +73,9 @@ public class PowerUI extends SystemUI { private PowerManager mPowerManager; private WarningsUI mWarnings; private final Configuration mLastConfiguration = new Configuration(); - private long mTimeRemaining = Long.MAX_VALUE; private int mPlugType = 0; private int mInvalidCharger = 0; private EnhancedEstimates mEnhancedEstimates; - private Estimate mLastEstimate; - private boolean mLowWarningShownThisChargeCycle; - private boolean mSevereWarningShownThisChargeCycle; private Future mLastShowWarningTask; private boolean mEnableSkinTemperatureWarning; private boolean mEnableUsbTemperatureAlarm; @@ -87,6 +85,10 @@ public class PowerUI extends SystemUI { private long mScreenOffTime = -1; + @VisibleForTesting boolean mLowWarningShownThisChargeCycle; + @VisibleForTesting boolean mSevereWarningShownThisChargeCycle; + @VisibleForTesting BatteryStateSnapshot mCurrentBatteryStateSnapshot; + @VisibleForTesting BatteryStateSnapshot mLastBatteryStateSnapshot; @VisibleForTesting IThermalService mThermalService; @VisibleForTesting int mBatteryLevel = 100; @@ -205,6 +207,7 @@ public class PowerUI extends SystemUI { mPlugType = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 1); final int oldInvalidCharger = mInvalidCharger; mInvalidCharger = intent.getIntExtra(BatteryManager.EXTRA_INVALID_CHARGER, 0); + mLastBatteryStateSnapshot = mCurrentBatteryStateSnapshot; final boolean plugged = mPlugType != 0; final boolean oldPlugged = oldPlugType != 0; @@ -233,16 +236,22 @@ public class PowerUI extends SystemUI { mWarnings.dismissInvalidChargerWarning(); } else if (mWarnings.isInvalidChargerWarningShowing()) { // if invalid charger is showing, don't show low battery + if (DEBUG) { + Slog.d(TAG, "Bad Charger"); + } return; } // Show the correct version of low battery warning if needed if (mLastShowWarningTask != null) { mLastShowWarningTask.cancel(true); + if (DEBUG) { + Slog.d(TAG, "cancelled task"); + } } mLastShowWarningTask = ThreadUtils.postOnBackgroundThread(() -> { - maybeShowBatteryWarning( - oldBatteryLevel, plugged, oldPlugged, oldBucket, bucket); + maybeShowBatteryWarningV2( + plugged, bucket); }); } else if (Intent.ACTION_SCREEN_OFF.equals(action)) { @@ -257,118 +266,176 @@ public class PowerUI extends SystemUI { } } - protected void maybeShowBatteryWarning(int oldBatteryLevel, boolean plugged, boolean oldPlugged, - int oldBucket, int bucket) { - boolean isPowerSaver = mPowerManager.isPowerSaveMode(); - // only play SFX when the dialog comes up or the bucket changes - final boolean playSound = bucket != oldBucket || oldPlugged; + protected void maybeShowBatteryWarningV2(boolean plugged, int bucket) { final boolean hybridEnabled = mEnhancedEstimates.isHybridNotificationEnabled(); + final boolean isPowerSaverMode = mPowerManager.isPowerSaveMode(); + + // Stick current battery state into an immutable container to determine if we should show + // a warning. + if (DEBUG) { + Slog.d(TAG, "evaluating which notification to show"); + } if (hybridEnabled) { - Estimate estimate = mLastEstimate; - if (estimate == null || mBatteryLevel != oldBatteryLevel) { - estimate = mEnhancedEstimates.getEstimate(); - mLastEstimate = estimate; + if (DEBUG) { + Slog.d(TAG, "using hybrid"); } - // Turbo is not always booted once SysUI is running so we have to make sure we actually - // get data back - if (estimate != null) { - mTimeRemaining = estimate.estimateMillis; - mWarnings.updateEstimate(estimate); - mWarnings.updateThresholds(mEnhancedEstimates.getLowWarningThreshold(), - mEnhancedEstimates.getSevereWarningThreshold()); - - // if we are now over 45% battery & 6 hours remaining we can trigger hybrid - // notification again - if (mBatteryLevel >= CHARGE_CYCLE_PERCENT_RESET - && mTimeRemaining > SIX_HOURS_MILLIS) { - mLowWarningShownThisChargeCycle = false; - mSevereWarningShownThisChargeCycle = false; - } + Estimate estimate = refreshEstimateIfNeeded(); + mCurrentBatteryStateSnapshot = new BatteryStateSnapshot(mBatteryLevel, isPowerSaverMode, + plugged, bucket, mBatteryStatus, mLowBatteryReminderLevels[1], + mLowBatteryReminderLevels[0], estimate.getEstimateMillis(), + mEnhancedEstimates.getSevereWarningThreshold(), + mEnhancedEstimates.getLowWarningThreshold(), estimate.isBasedOnUsage()); + } else { + if (DEBUG) { + Slog.d(TAG, "using standard"); } + mCurrentBatteryStateSnapshot = new BatteryStateSnapshot(mBatteryLevel, isPowerSaverMode, + plugged, bucket, mBatteryStatus, mLowBatteryReminderLevels[1], + mLowBatteryReminderLevels[0]); } - if (shouldShowLowBatteryWarning(plugged, oldPlugged, oldBucket, bucket, - mTimeRemaining, isPowerSaver, mBatteryStatus)) { - mWarnings.showLowBatteryWarning(playSound); + mWarnings.updateSnapshot(mCurrentBatteryStateSnapshot); + if (mCurrentBatteryStateSnapshot.isHybrid()) { + maybeShowHybridWarning(mCurrentBatteryStateSnapshot, mLastBatteryStateSnapshot); + } else { + maybeShowBatteryWarning(mCurrentBatteryStateSnapshot, mLastBatteryStateSnapshot); + } + } + // updates the time estimate if we don't have one or battery level has changed. + @VisibleForTesting + Estimate refreshEstimateIfNeeded() { + if (mLastBatteryStateSnapshot == null + || mLastBatteryStateSnapshot.getTimeRemainingMillis() == NO_ESTIMATE_AVAILABLE + || mBatteryLevel != mLastBatteryStateSnapshot.getBatteryLevel()) { + final Estimate estimate = mEnhancedEstimates.getEstimate(); + if (DEBUG) { + Slog.d(TAG, "updated estimate: " + estimate.getEstimateMillis()); + } + return estimate; + } + return new Estimate(mLastBatteryStateSnapshot.getTimeRemainingMillis(), + mLastBatteryStateSnapshot.isBasedOnUsage()); + } + + @VisibleForTesting + void maybeShowHybridWarning(BatteryStateSnapshot currentSnapshot, + BatteryStateSnapshot lastSnapshot) { + // if we are now over 45% battery & 6 hours remaining so we can trigger hybrid + // notification again + if (currentSnapshot.getBatteryLevel() >= CHARGE_CYCLE_PERCENT_RESET + && currentSnapshot.getTimeRemainingMillis() > SIX_HOURS_MILLIS) { + mLowWarningShownThisChargeCycle = false; + mSevereWarningShownThisChargeCycle = false; + if (DEBUG) { + Slog.d(TAG, "Charge cycle reset! Can show warnings again"); + } + } + + final boolean playSound = currentSnapshot.getBucket() != lastSnapshot.getBucket() + || lastSnapshot.getPlugged(); + + if (shouldShowHybridWarning(currentSnapshot)) { + mWarnings.showLowBatteryWarning(playSound); // mark if we've already shown a warning this cycle. This will prevent the notification // trigger from spamming users by only showing low/critical warnings once per cycle - if (hybridEnabled) { - if (mTimeRemaining <= mEnhancedEstimates.getSevereWarningThreshold() - || mBatteryLevel <= mLowBatteryReminderLevels[1]) { - mSevereWarningShownThisChargeCycle = true; - mLowWarningShownThisChargeCycle = true; - } else { - mLowWarningShownThisChargeCycle = true; + if (currentSnapshot.getTimeRemainingMillis() + <= currentSnapshot.getSevereLevelThreshold() + || currentSnapshot.getBatteryLevel() <= mLowBatteryReminderLevels[1]) { + mSevereWarningShownThisChargeCycle = true; + mLowWarningShownThisChargeCycle = true; + if (DEBUG) { + Slog.d(TAG, "Severe warning marked as shown this cycle"); } + } else { + Slog.d(TAG, "Low warning marked as shown this cycle"); + mLowWarningShownThisChargeCycle = true; + } + + } else if (shouldDismissHybridWarning(currentSnapshot)) { + if (DEBUG) { + Slog.d(TAG, "Dismissing warning"); } - } else if (shouldDismissLowBatteryWarning(plugged, oldBucket, bucket, mTimeRemaining, - isPowerSaver)) { mWarnings.dismissLowBatteryWarning(); } else { + if (DEBUG) { + Slog.d(TAG, "Updating warning"); + } mWarnings.updateLowBatteryWarning(); } } @VisibleForTesting - boolean shouldShowLowBatteryWarning(boolean plugged, boolean oldPlugged, int oldBucket, - int bucket, long timeRemaining, boolean isPowerSaver, int batteryStatus) { - if (mEnhancedEstimates.isHybridNotificationEnabled()) { - // triggering logic when enhanced estimate is available - return isEnhancedTrigger(plugged, timeRemaining, isPowerSaver, batteryStatus); - } - // legacy triggering logic - return !plugged - && !isPowerSaver - && (((bucket < oldBucket || oldPlugged) && bucket < 0)) - && batteryStatus != BatteryManager.BATTERY_STATUS_UNKNOWN; - } - - @VisibleForTesting - boolean shouldDismissLowBatteryWarning(boolean plugged, int oldBucket, int bucket, - long timeRemaining, boolean isPowerSaver) { - final boolean hybridEnabled = mEnhancedEstimates.isHybridNotificationEnabled(); - final boolean hybridWouldDismiss = hybridEnabled - && timeRemaining > mEnhancedEstimates.getLowWarningThreshold(); - final boolean standardWouldDismiss = (bucket > oldBucket && bucket > 0); - return (isPowerSaver && !hybridEnabled) - || plugged - || (standardWouldDismiss && (!mEnhancedEstimates.isHybridNotificationEnabled() - || hybridWouldDismiss)); - } - - private boolean isEnhancedTrigger(boolean plugged, long timeRemaining, boolean isPowerSaver, - int batteryStatus) { - if (plugged || batteryStatus == BatteryManager.BATTERY_STATUS_UNKNOWN) { + boolean shouldShowHybridWarning(BatteryStateSnapshot snapshot) { + if (snapshot.getPlugged() + || snapshot.getBatteryStatus() == BatteryManager.BATTERY_STATUS_UNKNOWN) { + Slog.d(TAG, "can't show warning due to - plugged: " + snapshot.getPlugged() + + " status unknown: " + + (snapshot.getBatteryStatus() == BatteryManager.BATTERY_STATUS_UNKNOWN)); return false; } - int warnLevel = mLowBatteryReminderLevels[0]; - int critLevel = mLowBatteryReminderLevels[1]; // Only show the low warning once per charge cycle & no battery saver - final boolean canShowWarning = !mLowWarningShownThisChargeCycle && !isPowerSaver - && (timeRemaining < mEnhancedEstimates.getLowWarningThreshold() - || mBatteryLevel <= warnLevel); + final boolean canShowWarning = !mLowWarningShownThisChargeCycle && !snapshot.isPowerSaver() + && (snapshot.getTimeRemainingMillis() < snapshot.getLowThresholdMillis() + || snapshot.getBatteryLevel() <= snapshot.getLowLevelThreshold()); // Only show the severe warning once per charge cycle final boolean canShowSevereWarning = !mSevereWarningShownThisChargeCycle - && (timeRemaining < mEnhancedEstimates.getSevereWarningThreshold() - || mBatteryLevel <= critLevel); + && (snapshot.getTimeRemainingMillis() < snapshot.getSevereThresholdMillis() + || snapshot.getBatteryLevel() <= snapshot.getSevereLevelThreshold()); final boolean canShow = canShowWarning || canShowSevereWarning; if (DEBUG) { - Slog.d(TAG, "Enhanced trigger is: " + canShow + "\nwith values: " + Slog.d(TAG, "Enhanced trigger is: " + canShow + "\nwith battery snapshot:" + " mLowWarningShownThisChargeCycle: " + mLowWarningShownThisChargeCycle + " mSevereWarningShownThisChargeCycle: " + mSevereWarningShownThisChargeCycle - + " mEnhancedEstimates.timeremaining: " + timeRemaining - + " mBatteryLevel: " + mBatteryLevel - + " canShowWarning: " + canShowWarning - + " canShowSevereWarning: " + canShowSevereWarning - + " plugged: " + plugged - + " batteryStatus: " + batteryStatus - + " isPowerSaver: " + isPowerSaver); + + "\n" + snapshot.toString()); + } + return canShow; + } + + @VisibleForTesting + boolean shouldDismissHybridWarning(BatteryStateSnapshot snapshot) { + return snapshot.getPlugged() + || snapshot.getTimeRemainingMillis() > snapshot.getLowThresholdMillis(); + } + + protected void maybeShowBatteryWarning( + BatteryStateSnapshot currentSnapshot, + BatteryStateSnapshot lastSnapshot) { + final boolean playSound = currentSnapshot.getBucket() != lastSnapshot.getBucket() + || lastSnapshot.getPlugged(); + + if (shouldShowLowBatteryWarning(currentSnapshot, lastSnapshot)) { + mWarnings.showLowBatteryWarning(playSound); + } else if (shouldDismissLowBatteryWarning(currentSnapshot, lastSnapshot)) { + mWarnings.dismissLowBatteryWarning(); + } else { + mWarnings.updateLowBatteryWarning(); } - return canShowWarning || canShowSevereWarning; + } + + @VisibleForTesting + boolean shouldShowLowBatteryWarning( + BatteryStateSnapshot currentSnapshot, + BatteryStateSnapshot lastSnapshot) { + return !currentSnapshot.getPlugged() + && !currentSnapshot.isPowerSaver() + && (((currentSnapshot.getBucket() < lastSnapshot.getBucket() + || lastSnapshot.getPlugged()) + && currentSnapshot.getBucket() < 0)) + && currentSnapshot.getBatteryStatus() != BatteryManager.BATTERY_STATUS_UNKNOWN; + } + + @VisibleForTesting + boolean shouldDismissLowBatteryWarning( + BatteryStateSnapshot currentSnapshot, + BatteryStateSnapshot lastSnapshot) { + return currentSnapshot.isPowerSaver() + || currentSnapshot.getPlugged() + || (currentSnapshot.getBucket() > lastSnapshot.getBucket() + && currentSnapshot.getBucket() > 0); } private void initTemperature() { @@ -453,12 +520,20 @@ public class PowerUI extends SystemUI { mWarnings.dump(pw); } + /** + * The interface to allow PowerUI to communicate with whatever implementation of WarningsUI + * is being used by the system. + */ public interface WarningsUI { - void update(int batteryLevel, int bucket, long screenOffTime); - void updateEstimate(Estimate estimate); - - void updateThresholds(long lowThreshold, long severeThreshold); + /** + * Updates battery and screen info for determining whether to trigger battery warnings or + * not. + * @param batteryLevel The current battery level + * @param bucket The current battery bucket + * @param screenOffTime How long the screen has been off in millis + */ + void update(int batteryLevel, int bucket, long screenOffTime); void dismissLowBatteryWarning(); @@ -486,6 +561,12 @@ public class PowerUI extends SystemUI { void dump(PrintWriter pw); void userSwitched(); + + /** + * Updates the snapshot of battery state used for evaluating battery warnings + * @param snapshot object containing relevant values for making battery warning decisions. + */ + void updateSnapshot(BatteryStateSnapshot snapshot); } // Thermal event received from thermal service manager subsystem diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java index a35488518faa..142f398cd881 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java @@ -20,9 +20,6 @@ import static com.android.systemui.SysUiServiceProvider.getComponent; import static com.android.systemui.statusbar.notification.ActivityLaunchAnimator.ExpandAnimationParameters; import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEXT; - -import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEXT; - import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; @@ -2956,6 +2953,9 @@ public class NotificationPanelView extends PanelView implements if (mKeyguardStatusBar != null) { mKeyguardStatusBar.dump(fd, pw, args); } + if (mKeyguardStatusView != null) { + mKeyguardStatusView.dump(fd, pw, args); + } } public boolean hasActiveClearableNotifications() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java index af3c96f73642..3fa3e1a6e6d4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java @@ -226,7 +226,7 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC String percentage = NumberFormat.getPercentInstance().format((double) mLevel / 100.0); return PowerUtil.getBatteryRemainingShortStringFormatted( - mContext, mEstimate.estimateMillis); + mContext, mEstimate.getEstimateMillis()); } private void updateEstimateInBackground() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java index 5876ae1910be..58c931190c83 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java @@ -30,6 +30,7 @@ import static org.mockito.Mockito.verify; import android.app.Notification; import android.app.NotificationManager; +import android.os.BatteryManager; import android.test.suitebuilder.annotation.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -57,6 +58,9 @@ public class PowerNotificationWarningsTest extends SysuiTestCase { // Test Instance. mContext.addMockSystemService(NotificationManager.class, mMockNotificationManager); mPowerNotificationWarnings = new PowerNotificationWarnings(mContext); + BatteryStateSnapshot snapshot = new BatteryStateSnapshot(100, false, false, 1, + BatteryManager.BATTERY_HEALTH_GOOD, 5, 15); + mPowerNotificationWarnings.updateSnapshot(snapshot); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java b/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java index 0aed63d25112..f51e4731a390 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java @@ -17,8 +17,7 @@ package com.android.systemui.power; import static android.provider.Settings.Global.SHOW_TEMPERATURE_WARNING; import static android.provider.Settings.Global.SHOW_USB_TEMPERATURE_ALARM; -import static junit.framework.Assert.assertFalse; -import static junit.framework.Assert.assertTrue; +import static com.google.common.truth.Truth.assertThat; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.anyObject; @@ -29,7 +28,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; -import android.content.Intent; import android.os.BatteryManager; import android.os.IThermalEventListener; import android.os.IThermalService; @@ -42,22 +40,20 @@ import android.testing.TestableLooper; import android.testing.TestableLooper.RunWithLooper; import android.testing.TestableResources; -import com.android.settingslib.utils.ThreadUtils; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.power.PowerUI.WarningsUI; import com.android.systemui.statusbar.phone.StatusBar; +import java.time.Duration; +import java.util.concurrent.TimeUnit; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import java.time.Duration; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - @RunWith(AndroidTestingRunner.class) @RunWithLooper @SmallTest @@ -75,6 +71,7 @@ public class PowerUITest extends SysuiTestCase { private static final int OLD_BATTERY_LEVEL_NINE = 9; private static final int OLD_BATTERY_LEVEL_10 = 10; private static final long VERY_BELOW_SEVERE_HYBRID_THRESHOLD = TimeUnit.MINUTES.toMillis(15); + public static final int BATTERY_LEVEL_10 = 10; private WarningsUI mMockWarnings; private PowerUI mPowerUI; private EnhancedEstimates mEnhancedEstimates; @@ -176,368 +173,333 @@ public class PowerUITest extends SysuiTestCase { } @Test - public void testShouldShowLowBatteryWarning_showHybridOnly_overrideThresholdHigh_returnsNoShow() { - when(mEnhancedEstimates.isHybridNotificationEnabled()).thenReturn(true); - when(mEnhancedEstimates.getLowWarningThreshold()) - .thenReturn(Duration.ofHours(1).toMillis()); - when(mEnhancedEstimates.getSevereWarningThreshold()).thenReturn(ONE_HOUR_MILLIS); + public void testMaybeShowHybridWarning() { mPowerUI.start(); - // unplugged device that would not show the non-hybrid notification but would show the - // hybrid but the threshold has been overriden to be too low - boolean shouldShow = - mPowerUI.shouldShowLowBatteryWarning(UNPLUGGED, UNPLUGGED, ABOVE_WARNING_BUCKET, - ABOVE_WARNING_BUCKET, BELOW_HYBRID_THRESHOLD, - POWER_SAVER_OFF, BatteryManager.BATTERY_HEALTH_GOOD); - assertFalse(shouldShow); - } + // verify low warning shown this cycle noticed + BatteryStateSnapshotWrapper state = new BatteryStateSnapshotWrapper(); + BatteryStateSnapshot lastState = state.get(); + state.mTimeRemainingMillis = Duration.ofHours(2).toMillis(); + state.mBatteryLevel = 15; - @Test - public void testShouldShowLowBatteryWarning_showHybridOnly_overrideThresholdHigh_returnsShow() { - when(mEnhancedEstimates.isHybridNotificationEnabled()).thenReturn(true); - when(mEnhancedEstimates.getLowWarningThreshold()) - .thenReturn(Duration.ofHours(5).toMillis()); - when(mEnhancedEstimates.getSevereWarningThreshold()).thenReturn(ONE_HOUR_MILLIS); - mPowerUI.start(); + mPowerUI.maybeShowHybridWarning(state.get(), lastState); - // unplugged device that would not show the non-hybrid notification but would show the - // hybrid since the threshold has been overriden to be much higher - boolean shouldShow = - mPowerUI.shouldShowLowBatteryWarning(UNPLUGGED, UNPLUGGED, ABOVE_WARNING_BUCKET, - ABOVE_WARNING_BUCKET, ABOVE_HYBRID_THRESHOLD, - POWER_SAVER_OFF, BatteryManager.BATTERY_HEALTH_GOOD); - assertTrue(shouldShow); - } + assertThat(mPowerUI.mLowWarningShownThisChargeCycle).isTrue(); + assertThat(mPowerUI.mSevereWarningShownThisChargeCycle).isFalse(); - @Test - public void testShouldShowLowBatteryWarning_showHybridOnly_returnsShow() { - when(mEnhancedEstimates.isHybridNotificationEnabled()).thenReturn(true); - when(mEnhancedEstimates.getLowWarningThreshold()).thenReturn(PowerUI.THREE_HOURS_IN_MILLIS); - when(mEnhancedEstimates.getSevereWarningThreshold()).thenReturn(ONE_HOUR_MILLIS); - mPowerUI.start(); + // verify severe warning noticed this cycle + lastState = state.get(); + state.mBatteryLevel = 1; + state.mTimeRemainingMillis = Duration.ofMinutes(10).toMillis(); - // unplugged device that would not show the non-hybrid notification but would show the - // hybrid - boolean shouldShow = - mPowerUI.shouldShowLowBatteryWarning(UNPLUGGED, UNPLUGGED, ABOVE_WARNING_BUCKET, - ABOVE_WARNING_BUCKET, BELOW_HYBRID_THRESHOLD, - POWER_SAVER_OFF, BatteryManager.BATTERY_HEALTH_GOOD); - assertTrue(shouldShow); - } + mPowerUI.maybeShowHybridWarning(state.get(), lastState); - @Test - public void testShouldShowLowBatteryWarning_showHybrid_showStandard_returnsShow() { - when(mEnhancedEstimates.isHybridNotificationEnabled()).thenReturn(true); - when(mEnhancedEstimates.getLowWarningThreshold()).thenReturn(PowerUI.THREE_HOURS_IN_MILLIS); - when(mEnhancedEstimates.getSevereWarningThreshold()).thenReturn(ONE_HOUR_MILLIS); - mPowerUI.mBatteryLevel = 10; - mPowerUI.start(); + assertThat(mPowerUI.mLowWarningShownThisChargeCycle).isTrue(); + assertThat(mPowerUI.mSevereWarningShownThisChargeCycle).isTrue(); - // unplugged device that would show the non-hybrid notification and the hybrid - boolean shouldShow = - mPowerUI.shouldShowLowBatteryWarning(UNPLUGGED, UNPLUGGED, ABOVE_WARNING_BUCKET, - BELOW_WARNING_BUCKET, BELOW_HYBRID_THRESHOLD, - POWER_SAVER_OFF, BatteryManager.BATTERY_HEALTH_GOOD); - assertTrue(shouldShow); - } + // verify getting past threshold resets values + lastState = state.get(); + state.mBatteryLevel = 100; + state.mTimeRemainingMillis = Duration.ofDays(1).toMillis(); - @Test - public void testShouldShowLowBatteryWarning_showStandardOnly_returnsShow() { - when(mEnhancedEstimates.isHybridNotificationEnabled()).thenReturn(true); - when(mEnhancedEstimates.getLowWarningThreshold()).thenReturn(PowerUI.THREE_HOURS_IN_MILLIS); - when(mEnhancedEstimates.getSevereWarningThreshold()).thenReturn(ONE_HOUR_MILLIS); - mPowerUI.mBatteryLevel = 10; - mPowerUI.start(); + mPowerUI.maybeShowHybridWarning(state.get(), lastState); - // unplugged device that would show the non-hybrid but not the hybrid - boolean shouldShow = - mPowerUI.shouldShowLowBatteryWarning(UNPLUGGED, UNPLUGGED, ABOVE_WARNING_BUCKET, - BELOW_WARNING_BUCKET, ABOVE_HYBRID_THRESHOLD, - POWER_SAVER_OFF, BatteryManager.BATTERY_HEALTH_GOOD); - assertTrue(shouldShow); + assertThat(mPowerUI.mLowWarningShownThisChargeCycle).isFalse(); + assertThat(mPowerUI.mSevereWarningShownThisChargeCycle).isFalse(); } @Test - public void testShouldShowLowBatteryWarning_deviceHighBattery_returnsNoShow() { - when(mEnhancedEstimates.isHybridNotificationEnabled()).thenReturn(true); - when(mEnhancedEstimates.getLowWarningThreshold()).thenReturn(PowerUI.THREE_HOURS_IN_MILLIS); - when(mEnhancedEstimates.getSevereWarningThreshold()).thenReturn(ONE_HOUR_MILLIS); + public void testShouldShowHybridWarning_lowLevelWarning() { mPowerUI.start(); - - // unplugged device that would show the neither due to battery level being good - boolean shouldShow = - mPowerUI.shouldShowLowBatteryWarning(UNPLUGGED, UNPLUGGED, ABOVE_WARNING_BUCKET, - ABOVE_WARNING_BUCKET, ABOVE_HYBRID_THRESHOLD, - POWER_SAVER_OFF, BatteryManager.BATTERY_HEALTH_GOOD); - assertFalse(shouldShow); + mPowerUI.mLowWarningShownThisChargeCycle = false; + mPowerUI.mSevereWarningShownThisChargeCycle = false; + BatteryStateSnapshotWrapper state = new BatteryStateSnapshotWrapper(); + + // sanity check to make sure we can show for a valid config + state.mBatteryLevel = 10; + state.mTimeRemainingMillis = Duration.ofHours(2).toMillis(); + boolean shouldShow = mPowerUI.shouldShowHybridWarning(state.get()); + assertThat(shouldShow).isTrue(); + + // Shouldn't show if plugged in + state.mPlugged = true; + shouldShow = mPowerUI.shouldShowHybridWarning(state.get()); + assertThat(shouldShow).isFalse(); + + // Shouldn't show if battery is unknown + state.mPlugged = false; + state.mBatteryStatus = BatteryManager.BATTERY_STATUS_UNKNOWN; + shouldShow = mPowerUI.shouldShowHybridWarning(state.get()); + assertThat(shouldShow).isFalse(); + + state.mBatteryStatus = BatteryManager.BATTERY_HEALTH_GOOD; + // Already shown both warnings + mPowerUI.mLowWarningShownThisChargeCycle = true; + mPowerUI.mSevereWarningShownThisChargeCycle = true; + shouldShow = mPowerUI.shouldShowHybridWarning(state.get()); + assertThat(shouldShow).isFalse(); + + // Can show low warning + mPowerUI.mLowWarningShownThisChargeCycle = false; + shouldShow = mPowerUI.shouldShowHybridWarning(state.get()); + assertThat(shouldShow).isTrue(); + + // Can't show if above the threshold for time & battery + state.mTimeRemainingMillis = Duration.ofHours(1000).toMillis(); + state.mBatteryLevel = 100; + shouldShow = mPowerUI.shouldShowHybridWarning(state.get()); + assertThat(shouldShow).isFalse(); + + // Battery under low percentage threshold but not time + state.mBatteryLevel = 10; + state.mLowLevelThreshold = 50; + shouldShow = mPowerUI.shouldShowHybridWarning(state.get()); + assertThat(shouldShow).isTrue(); + + // Should also trigger if both level and time remaining under low threshold + state.mTimeRemainingMillis = Duration.ofHours(2).toMillis(); + shouldShow = mPowerUI.shouldShowHybridWarning(state.get()); + assertThat(shouldShow).isTrue(); + + // battery saver should block the low level warning though + state.mIsPowerSaver = true; + shouldShow = mPowerUI.shouldShowHybridWarning(state.get()); + assertThat(shouldShow).isFalse(); } @Test - public void testShouldShowLowBatteryWarning_devicePlugged_returnsNoShow() { - when(mEnhancedEstimates.isHybridNotificationEnabled()).thenReturn(true); - when(mEnhancedEstimates.getLowWarningThreshold()).thenReturn(PowerUI.THREE_HOURS_IN_MILLIS); - when(mEnhancedEstimates.getSevereWarningThreshold()).thenReturn(ONE_HOUR_MILLIS); - mPowerUI.start(); - - // plugged device that would show the neither due to being plugged - boolean shouldShow = - mPowerUI.shouldShowLowBatteryWarning(!UNPLUGGED, UNPLUGGED, ABOVE_WARNING_BUCKET, - BELOW_WARNING_BUCKET, BELOW_HYBRID_THRESHOLD, - POWER_SAVER_OFF, BatteryManager.BATTERY_HEALTH_GOOD); - assertFalse(shouldShow); - } - - @Test - public void testShouldShowLowBatteryWarning_deviceBatteryStatusUnknown_returnsNoShow() { - when(mEnhancedEstimates.isHybridNotificationEnabled()).thenReturn(true); - when(mEnhancedEstimates.getLowWarningThreshold()).thenReturn(PowerUI.THREE_HOURS_IN_MILLIS); - when(mEnhancedEstimates.getSevereWarningThreshold()).thenReturn(ONE_HOUR_MILLIS); + public void testShouldShowHybridWarning_severeLevelWarning() { mPowerUI.start(); - - // Unknown battery status device that would show the neither due to the battery status being - // unknown - boolean shouldShow = - mPowerUI.shouldShowLowBatteryWarning(UNPLUGGED, UNPLUGGED, ABOVE_WARNING_BUCKET, - BELOW_WARNING_BUCKET, BELOW_HYBRID_THRESHOLD, - !POWER_SAVER_OFF, BatteryManager.BATTERY_STATUS_UNKNOWN); - assertFalse(shouldShow); + mPowerUI.mLowWarningShownThisChargeCycle = false; + mPowerUI.mSevereWarningShownThisChargeCycle = false; + BatteryStateSnapshotWrapper state = new BatteryStateSnapshotWrapper(); + + // sanity check to make sure we can show for a valid config + state.mBatteryLevel = 1; + state.mTimeRemainingMillis = Duration.ofMinutes(1).toMillis(); + boolean shouldShow = mPowerUI.shouldShowHybridWarning(state.get()); + assertThat(shouldShow).isTrue(); + + // Shouldn't show if plugged in + state.mPlugged = true; + shouldShow = mPowerUI.shouldShowHybridWarning(state.get()); + assertThat(shouldShow).isFalse(); + + // Shouldn't show if battery is unknown + state.mPlugged = false; + state.mBatteryStatus = BatteryManager.BATTERY_STATUS_UNKNOWN; + shouldShow = mPowerUI.shouldShowHybridWarning(state.get()); + assertThat(shouldShow).isFalse(); + + state.mBatteryStatus = BatteryManager.BATTERY_HEALTH_GOOD; + // Already shown both warnings + mPowerUI.mLowWarningShownThisChargeCycle = true; + mPowerUI.mSevereWarningShownThisChargeCycle = true; + shouldShow = mPowerUI.shouldShowHybridWarning(state.get()); + assertThat(shouldShow).isFalse(); + + // Can show severe warning + mPowerUI.mSevereWarningShownThisChargeCycle = false; + shouldShow = mPowerUI.shouldShowHybridWarning(state.get()); + assertThat(shouldShow).isTrue(); + + // Can't show if above the threshold for time & battery + state.mTimeRemainingMillis = Duration.ofHours(1000).toMillis(); + state.mBatteryLevel = 100; + shouldShow = mPowerUI.shouldShowHybridWarning(state.get()); + assertThat(shouldShow).isFalse(); + + // Battery under low percentage threshold but not time + state.mBatteryLevel = 1; + state.mSevereLevelThreshold = 5; + shouldShow = mPowerUI.shouldShowHybridWarning(state.get()); + assertThat(shouldShow).isTrue(); + + // Should also trigger if both level and time remaining under low threshold + state.mTimeRemainingMillis = Duration.ofHours(2).toMillis(); + shouldShow = mPowerUI.shouldShowHybridWarning(state.get()); + assertThat(shouldShow).isTrue(); + + // battery saver should not block the severe level warning though + state.mIsPowerSaver = true; + shouldShow = mPowerUI.shouldShowHybridWarning(state.get()); + assertThat(shouldShow).isTrue(); } @Test - public void testShouldShowLowBatteryWarning_batterySaverEnabled_returnsNoShow() { - when(mEnhancedEstimates.isHybridNotificationEnabled()).thenReturn(true); - when(mEnhancedEstimates.getLowWarningThreshold()).thenReturn(PowerUI.THREE_HOURS_IN_MILLIS); - when(mEnhancedEstimates.getSevereWarningThreshold()).thenReturn(ONE_HOUR_MILLIS); + public void testShouldDismissHybridWarning() { mPowerUI.start(); - - // BatterySaverEnabled device that would show the neither due to battery saver - boolean shouldShow = - mPowerUI.shouldShowLowBatteryWarning(UNPLUGGED, UNPLUGGED, ABOVE_WARNING_BUCKET, - BELOW_WARNING_BUCKET, BELOW_HYBRID_THRESHOLD, - !POWER_SAVER_OFF, BatteryManager.BATTERY_HEALTH_GOOD); - assertFalse(shouldShow); + BatteryStateSnapshotWrapper state = new BatteryStateSnapshotWrapper(); + + // We should dismiss if the device is plugged in + state.mPlugged = true; + state.mTimeRemainingMillis = Duration.ofHours(1).toMillis(); + state.mLowThresholdMillis = Duration.ofHours(2).toMillis(); + boolean shouldDismiss = mPowerUI.shouldDismissHybridWarning(state.get()); + assertThat(shouldDismiss).isTrue(); + + // If not plugged in and below the threshold we should not dismiss + state.mPlugged = false; + shouldDismiss = mPowerUI.shouldDismissHybridWarning(state.get()); + assertThat(shouldDismiss).isFalse(); + + // If we go over the low warning threshold we should dismiss + state.mTimeRemainingMillis = Duration.ofHours(3).toMillis(); + shouldDismiss = mPowerUI.shouldDismissHybridWarning(state.get()); + assertThat(shouldDismiss).isTrue(); } @Test - public void testShouldShowLowBatteryWarning_onlyShowsOncePerChargeCycle() { + public void testRefreshEstimateIfNeeded_onlyQueriesEstimateOnBatteryLevelChangeOrNull() { mPowerUI.start(); + Estimate estimate = new Estimate(BELOW_HYBRID_THRESHOLD, true); when(mEnhancedEstimates.isHybridNotificationEnabled()).thenReturn(true); when(mEnhancedEstimates.getLowWarningThreshold()).thenReturn(PowerUI.THREE_HOURS_IN_MILLIS); when(mEnhancedEstimates.getSevereWarningThreshold()).thenReturn(ONE_HOUR_MILLIS); - when(mEnhancedEstimates.getEstimate()) - .thenReturn(new Estimate(BELOW_HYBRID_THRESHOLD, true)); - mPowerUI.mBatteryStatus = BatteryManager.BATTERY_HEALTH_GOOD; - - mPowerUI.maybeShowBatteryWarning(OLD_BATTERY_LEVEL_NINE, UNPLUGGED, UNPLUGGED, - ABOVE_WARNING_BUCKET, ABOVE_WARNING_BUCKET); - - // reduce battery level to handle time based trigger -> level trigger interactions + when(mEnhancedEstimates.getEstimate()).thenReturn(estimate); mPowerUI.mBatteryLevel = 10; - boolean shouldShow = - mPowerUI.shouldShowLowBatteryWarning(UNPLUGGED, UNPLUGGED, ABOVE_WARNING_BUCKET, - ABOVE_WARNING_BUCKET, BELOW_HYBRID_THRESHOLD, - POWER_SAVER_OFF, BatteryManager.BATTERY_HEALTH_GOOD); - assertFalse(shouldShow); - } - - @Test - public void testShouldDismissLowBatteryWarning_dismissWhenPowerSaverEnabledLegacy() { - mPowerUI.start(); - when(mEnhancedEstimates.isHybridNotificationEnabled()).thenReturn(false); - when(mEnhancedEstimates.getLowWarningThreshold()).thenReturn(PowerUI.THREE_HOURS_IN_MILLIS); - when(mEnhancedEstimates.getSevereWarningThreshold()).thenReturn(ONE_HOUR_MILLIS); - - // device that gets power saver turned on should dismiss - boolean shouldDismiss = - mPowerUI.shouldDismissLowBatteryWarning(UNPLUGGED, BELOW_WARNING_BUCKET, - BELOW_WARNING_BUCKET, ABOVE_HYBRID_THRESHOLD, !POWER_SAVER_OFF); - assertTrue(shouldDismiss); - } - @Test - public void testShouldNotDismissLowBatteryWarning_dismissWhenPowerSaverEnabledHybrid() { - mPowerUI.start(); - when(mEnhancedEstimates.isHybridNotificationEnabled()).thenReturn(true); - when(mEnhancedEstimates.getLowWarningThreshold()).thenReturn(PowerUI.THREE_HOURS_IN_MILLIS); - when(mEnhancedEstimates.getSevereWarningThreshold()).thenReturn(ONE_HOUR_MILLIS); - - // device that gets power saver turned on should dismiss - boolean shouldDismiss = - mPowerUI.shouldDismissLowBatteryWarning(UNPLUGGED, BELOW_WARNING_BUCKET, - BELOW_WARNING_BUCKET, ABOVE_HYBRID_THRESHOLD, !POWER_SAVER_OFF); - assertFalse(shouldDismiss); - } - - @Test - public void testShouldDismissLowBatteryWarning_dismissWhenPlugged() { - mPowerUI.start(); - when(mEnhancedEstimates.isHybridNotificationEnabled()).thenReturn(true); - when(mEnhancedEstimates.getLowWarningThreshold()).thenReturn(PowerUI.THREE_HOURS_IN_MILLIS); - when(mEnhancedEstimates.getSevereWarningThreshold()).thenReturn(ONE_HOUR_MILLIS); - - // device that gets plugged in should dismiss - boolean shouldDismiss = - mPowerUI.shouldDismissLowBatteryWarning(!UNPLUGGED, BELOW_WARNING_BUCKET, - BELOW_WARNING_BUCKET, ABOVE_HYBRID_THRESHOLD, POWER_SAVER_OFF); - assertTrue(shouldDismiss); - } - - @Test - public void testShouldDismissLowBatteryWarning_dismissHybridSignal_showStandardSignal_shouldShow() { - mPowerUI.start(); - when(mEnhancedEstimates.isHybridNotificationEnabled()).thenReturn(true); - when(mEnhancedEstimates.getLowWarningThreshold()).thenReturn(PowerUI.THREE_HOURS_IN_MILLIS); - when(mEnhancedEstimates.getSevereWarningThreshold()).thenReturn(ONE_HOUR_MILLIS); - - // would dismiss hybrid but not non-hybrid should not dismiss - boolean shouldDismiss = - mPowerUI.shouldDismissLowBatteryWarning(UNPLUGGED, BELOW_WARNING_BUCKET, - BELOW_WARNING_BUCKET, ABOVE_HYBRID_THRESHOLD, POWER_SAVER_OFF); - assertFalse(shouldDismiss); - } - - @Test - public void testShouldDismissLowBatteryWarning_showHybridSignal_dismissStandardSignal_shouldShow() { - mPowerUI.start(); - when(mEnhancedEstimates.isHybridNotificationEnabled()).thenReturn(true); - when(mEnhancedEstimates.getLowWarningThreshold()).thenReturn(PowerUI.THREE_HOURS_IN_MILLIS); - when(mEnhancedEstimates.getSevereWarningThreshold()).thenReturn(ONE_HOUR_MILLIS); - - // would dismiss non-hybrid but not hybrid should not dismiss - boolean shouldDismiss = - mPowerUI.shouldDismissLowBatteryWarning(UNPLUGGED, BELOW_WARNING_BUCKET, - ABOVE_WARNING_BUCKET, BELOW_HYBRID_THRESHOLD, POWER_SAVER_OFF); - assertFalse(shouldDismiss); - } - - @Test - public void testShouldDismissLowBatteryWarning_showBothSignal_shouldShow() { - mPowerUI.start(); - when(mEnhancedEstimates.isHybridNotificationEnabled()).thenReturn(true); - when(mEnhancedEstimates.getLowWarningThreshold()).thenReturn(PowerUI.THREE_HOURS_IN_MILLIS); - when(mEnhancedEstimates.getSevereWarningThreshold()).thenReturn(ONE_HOUR_MILLIS); - - // should not dismiss when both would not dismiss - boolean shouldDismiss = - mPowerUI.shouldDismissLowBatteryWarning(UNPLUGGED, BELOW_WARNING_BUCKET, - BELOW_WARNING_BUCKET, BELOW_HYBRID_THRESHOLD, POWER_SAVER_OFF); - assertFalse(shouldDismiss); - } - - @Test - public void testShouldDismissLowBatteryWarning_dismissBothSignal_shouldDismiss() { - mPowerUI.start(); - when(mEnhancedEstimates.isHybridNotificationEnabled()).thenReturn(true); - when(mEnhancedEstimates.getLowWarningThreshold()).thenReturn(PowerUI.THREE_HOURS_IN_MILLIS); - when(mEnhancedEstimates.getSevereWarningThreshold()).thenReturn(ONE_HOUR_MILLIS); - - //should dismiss if both would dismiss - boolean shouldDismiss = - mPowerUI.shouldDismissLowBatteryWarning(UNPLUGGED, BELOW_WARNING_BUCKET, - ABOVE_WARNING_BUCKET, ABOVE_HYBRID_THRESHOLD, POWER_SAVER_OFF); - assertTrue(shouldDismiss); - } - - @Test - public void testShouldDismissLowBatteryWarning_dismissStandardSignal_hybridDisabled_shouldDismiss() { - mPowerUI.start(); - when(mEnhancedEstimates.isHybridNotificationEnabled()).thenReturn(false); - when(mEnhancedEstimates.getLowWarningThreshold()).thenReturn(PowerUI.THREE_HOURS_IN_MILLIS); - when(mEnhancedEstimates.getSevereWarningThreshold()).thenReturn(ONE_HOUR_MILLIS); - - // would dismiss non-hybrid with hybrid disabled should dismiss - boolean shouldDismiss = - mPowerUI.shouldDismissLowBatteryWarning(UNPLUGGED, BELOW_WARNING_BUCKET, - ABOVE_WARNING_BUCKET, ABOVE_HYBRID_THRESHOLD, POWER_SAVER_OFF); - assertTrue(shouldDismiss); - } - - @Test - public void testShouldDismissLowBatteryWarning_powerSaverModeEnabled() - throws InterruptedException { - when(mPowerManager.isPowerSaveMode()).thenReturn(true); - - mPowerUI.start(); - mPowerUI.mReceiver.onReceive(mContext, - new Intent(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)); - - CountDownLatch latch = new CountDownLatch(1); - ThreadUtils.postOnBackgroundThread(() -> latch.countDown()); - latch.await(5, TimeUnit.SECONDS); - - verify(mMockWarnings).dismissLowBatteryWarning(); - } - - @Test - public void testShouldNotDismissLowBatteryWarning_powerSaverModeDisabled() - throws InterruptedException { - when(mPowerManager.isPowerSaveMode()).thenReturn(false); - - mPowerUI.start(); - mPowerUI.mReceiver.onReceive(mContext, - new Intent(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)); - - CountDownLatch latch = new CountDownLatch(1); - ThreadUtils.postOnBackgroundThread(() -> latch.countDown()); - latch.await(5, TimeUnit.SECONDS); + // we expect that the first time it will query since there is no last battery snapshot. + // However an invalid estimate (-1) is returned. + Estimate refreshedEstimate = mPowerUI.refreshEstimateIfNeeded(); + assertThat(refreshedEstimate.getEstimateMillis()).isEqualTo(BELOW_HYBRID_THRESHOLD); + BatteryStateSnapshot snapshot = new BatteryStateSnapshot( + BATTERY_LEVEL_10, false, false, 0, BatteryManager.BATTERY_HEALTH_GOOD, + 0, 0, -1, 0, 0, false); + mPowerUI.mLastBatteryStateSnapshot = snapshot; + + // query again since the estimate was -1 + estimate = new Estimate(BELOW_SEVERE_HYBRID_THRESHOLD, true); + when(mEnhancedEstimates.getEstimate()).thenReturn(estimate); + refreshedEstimate = mPowerUI.refreshEstimateIfNeeded(); + assertThat(refreshedEstimate.getEstimateMillis()).isEqualTo(BELOW_SEVERE_HYBRID_THRESHOLD); + snapshot = new BatteryStateSnapshot( + BATTERY_LEVEL_10, false, false, 0, BatteryManager.BATTERY_HEALTH_GOOD, 0, + 0, BELOW_SEVERE_HYBRID_THRESHOLD, 0, 0, false); + mPowerUI.mLastBatteryStateSnapshot = snapshot; + + // Battery level hasn't changed, so we don't query again + estimate = new Estimate(BELOW_HYBRID_THRESHOLD, true); + when(mEnhancedEstimates.getEstimate()).thenReturn(estimate); + refreshedEstimate = mPowerUI.refreshEstimateIfNeeded(); + assertThat(refreshedEstimate.getEstimateMillis()).isEqualTo(BELOW_SEVERE_HYBRID_THRESHOLD); - verify(mMockWarnings, never()).dismissLowBatteryWarning(); + // Battery level changes so we update again + mPowerUI.mBatteryLevel = 9; + refreshedEstimate = mPowerUI.refreshEstimateIfNeeded(); + assertThat(refreshedEstimate.getEstimateMillis()).isEqualTo(BELOW_HYBRID_THRESHOLD); } @Test - public void testSevereWarning_countsAsLowAndSevere_WarningOnlyShownOnce() { + public void testShouldShowStandardWarning() { mPowerUI.start(); - when(mEnhancedEstimates.isHybridNotificationEnabled()).thenReturn(true); - when(mEnhancedEstimates.getLowWarningThreshold()).thenReturn(PowerUI.THREE_HOURS_IN_MILLIS); - when(mEnhancedEstimates.getSevereWarningThreshold()).thenReturn(ONE_HOUR_MILLIS); - when(mEnhancedEstimates.getEstimate()) - .thenReturn(new Estimate(BELOW_SEVERE_HYBRID_THRESHOLD, true)); - mPowerUI.mBatteryStatus = BatteryManager.BATTERY_HEALTH_GOOD; - - // reduce battery level to handle time based trigger -> level trigger interactions - mPowerUI.mBatteryLevel = 5; - boolean shouldShow = - mPowerUI.shouldShowLowBatteryWarning(UNPLUGGED, UNPLUGGED, ABOVE_WARNING_BUCKET, - ABOVE_WARNING_BUCKET, BELOW_SEVERE_HYBRID_THRESHOLD, - POWER_SAVER_OFF, BatteryManager.BATTERY_HEALTH_GOOD); - assertTrue(shouldShow); - - // actually run the end to end since it handles changing the internal state. - mPowerUI.maybeShowBatteryWarning(OLD_BATTERY_LEVEL_10, UNPLUGGED, UNPLUGGED, - ABOVE_WARNING_BUCKET, ABOVE_WARNING_BUCKET); - - shouldShow = - mPowerUI.shouldShowLowBatteryWarning(UNPLUGGED, UNPLUGGED, ABOVE_WARNING_BUCKET, - ABOVE_WARNING_BUCKET, VERY_BELOW_SEVERE_HYBRID_THRESHOLD, - POWER_SAVER_OFF, BatteryManager.BATTERY_HEALTH_GOOD); - assertFalse(shouldShow); + BatteryStateSnapshotWrapper state = new BatteryStateSnapshotWrapper(); + state.mIsHybrid = false; + BatteryStateSnapshot lastState = state.get(); + + // sanity check to make sure we can show for a valid config + state.mBatteryLevel = 10; + state.mBucket = -1; + boolean shouldShow = mPowerUI.shouldShowLowBatteryWarning(state.get(), lastState); + assertThat(shouldShow).isTrue(); + lastState = state.get(); + + // Shouldn't show if plugged in + state.mPlugged = true; + shouldShow = mPowerUI.shouldShowLowBatteryWarning(state.get(), lastState); + assertThat(shouldShow).isFalse(); + + state.mPlugged = false; + // Shouldn't show if battery saver + state.mIsPowerSaver = true; + shouldShow = mPowerUI.shouldShowLowBatteryWarning(state.get(), lastState); + assertThat(shouldShow).isFalse(); + + state.mIsPowerSaver = false; + // Shouldn't show if battery is unknown + state.mPlugged = false; + state.mBatteryStatus = BatteryManager.BATTERY_STATUS_UNKNOWN; + shouldShow = mPowerUI.shouldShowLowBatteryWarning(state.get(), lastState); + assertThat(shouldShow).isFalse(); + + state.mBatteryStatus = BatteryManager.BATTERY_HEALTH_GOOD; + // show if plugged -> unplugged, bucket -1 -> -1 + state.mPlugged = true; + state.mBucket = -1; + lastState = state.get(); + state.mPlugged = false; + state.mBucket = -1; + shouldShow = mPowerUI.shouldShowLowBatteryWarning(state.get(), lastState); + assertThat(shouldShow).isTrue(); + + // don't show if plugged -> unplugged, bucket 0 -> 0 + state.mPlugged = true; + state.mBucket = 0; + lastState = state.get(); + state.mPlugged = false; + state.mBucket = 0; + shouldShow = mPowerUI.shouldShowLowBatteryWarning(state.get(), lastState); + assertThat(shouldShow).isFalse(); + + // show if unplugged -> unplugged, bucket 0 -> -1 + state.mPlugged = false; + state.mBucket = 0; + lastState = state.get(); + state.mPlugged = false; + state.mBucket = -1; + shouldShow = mPowerUI.shouldShowLowBatteryWarning(state.get(), lastState); + assertThat(shouldShow).isTrue(); + + // don't show if unplugged -> unplugged, bucket -1 -> 1 + state.mPlugged = false; + state.mBucket = -1; + lastState = state.get(); + state.mPlugged = false; + state.mBucket = 1; + shouldShow = mPowerUI.shouldShowLowBatteryWarning(state.get(), lastState); + assertThat(shouldShow).isFalse(); } @Test - public void testMaybeShowBatteryWarning_onlyQueriesEstimateOnBatteryLevelChangeOrNull() { + public void testShouldDismissStandardWarning() { mPowerUI.start(); - Estimate estimate = new Estimate(BELOW_HYBRID_THRESHOLD, true); - when(mEnhancedEstimates.isHybridNotificationEnabled()).thenReturn(true); - when(mEnhancedEstimates.getLowWarningThreshold()).thenReturn(PowerUI.THREE_HOURS_IN_MILLIS); - when(mEnhancedEstimates.getSevereWarningThreshold()).thenReturn(ONE_HOUR_MILLIS); - when(mEnhancedEstimates.getEstimate()).thenReturn(estimate); - mPowerUI.mBatteryStatus = BatteryManager.BATTERY_HEALTH_GOOD; - - // we expect that the first time it will query even if the level is the same - mPowerUI.mBatteryLevel = 9; - mPowerUI.maybeShowBatteryWarning(OLD_BATTERY_LEVEL_NINE, UNPLUGGED, UNPLUGGED, - ABOVE_WARNING_BUCKET, ABOVE_WARNING_BUCKET); - verify(mEnhancedEstimates, times(1)).getEstimate(); - - // We should NOT query again if the battery level hasn't changed - mPowerUI.maybeShowBatteryWarning(OLD_BATTERY_LEVEL_NINE, UNPLUGGED, UNPLUGGED, - ABOVE_WARNING_BUCKET, ABOVE_WARNING_BUCKET); - verify(mEnhancedEstimates, times(1)).getEstimate(); - - // Battery level has changed, so we should query again - mPowerUI.maybeShowBatteryWarning(OLD_BATTERY_LEVEL_10, UNPLUGGED, UNPLUGGED, - ABOVE_WARNING_BUCKET, ABOVE_WARNING_BUCKET); - verify(mEnhancedEstimates, times(2)).getEstimate(); + BatteryStateSnapshotWrapper state = new BatteryStateSnapshotWrapper(); + state.mIsHybrid = false; + BatteryStateSnapshot lastState = state.get(); + + // should dismiss if battery saver + state.mIsPowerSaver = true; + boolean shouldDismiss = mPowerUI.shouldDismissLowBatteryWarning(state.get(), lastState); + assertThat(shouldDismiss).isTrue(); + + state.mIsPowerSaver = false; + // should dismiss if plugged + state.mPlugged = true; + shouldDismiss = mPowerUI.shouldDismissLowBatteryWarning(state.get(), lastState); + assertThat(shouldDismiss).isTrue(); + + state.mPlugged = false; + // should dismiss if bucket 0 -> 1 + state.mBucket = 0; + lastState = state.get(); + state.mBucket = 1; + shouldDismiss = mPowerUI.shouldDismissLowBatteryWarning(state.get(), lastState); + assertThat(shouldDismiss).isTrue(); + + // shouldn't dismiss if bucket -1 -> 0 + state.mBucket = -1; + lastState = state.get(); + state.mBucket = 0; + shouldDismiss = mPowerUI.shouldDismissLowBatteryWarning(state.get(), lastState); + assertThat(shouldDismiss).isFalse(); + + // should dismiss if powersaver & bucket 0 -> 1 + state.mIsPowerSaver = true; + state.mBucket = 0; + lastState = state.get(); + state.mBucket = 1; + shouldDismiss = mPowerUI.shouldDismissLowBatteryWarning(state.get(), lastState); + assertThat(shouldDismiss).isTrue(); } private Temperature getEmergencyStatusTemp(int type, String name) { @@ -556,4 +518,35 @@ public class PowerUITest extends SysuiTestCase { mPowerUI.mComponents = mContext.getComponents(); mPowerUI.mThermalService = mThermalServiceMock; } + + /** + * A simple wrapper class that sets values by default and makes them not final to improve + * test clarity. + */ + private class BatteryStateSnapshotWrapper { + public int mBatteryLevel = 100; + public boolean mIsPowerSaver = false; + public boolean mPlugged = false; + public long mSevereThresholdMillis = Duration.ofHours(1).toMillis(); + public long mLowThresholdMillis = Duration.ofHours(3).toMillis(); + public int mSevereLevelThreshold = 5; + public int mLowLevelThreshold = 15; + public int mBucket = 1; + public int mBatteryStatus = BatteryManager.BATTERY_HEALTH_GOOD; + public long mTimeRemainingMillis = Duration.ofHours(24).toMillis(); + public boolean mIsBasedOnUsage = true; + public boolean mIsHybrid = true; + + public BatteryStateSnapshot get() { + if (mIsHybrid) { + return new BatteryStateSnapshot(mBatteryLevel, mIsPowerSaver, mPlugged, mBucket, + mBatteryStatus, mSevereLevelThreshold, mLowLevelThreshold, + mTimeRemainingMillis, mSevereThresholdMillis, mLowThresholdMillis, + mIsBasedOnUsage); + } else { + return new BatteryStateSnapshot(mBatteryLevel, mIsPowerSaver, mPlugged, mBucket, + mBatteryStatus, mSevereLevelThreshold, mLowLevelThreshold); + } + } + } } diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java index d2c39eaeafb7..da89116792ab 100644 --- a/services/core/java/com/android/server/ConnectivityService.java +++ b/services/core/java/com/android/server/ConnectivityService.java @@ -1575,8 +1575,7 @@ public class ConnectivityService extends IConnectivityManager.Stub public boolean isActiveNetworkMetered() { enforceAccessPermission(); - final int uid = Binder.getCallingUid(); - final NetworkCapabilities caps = getUnfilteredActiveNetworkState(uid).networkCapabilities; + final NetworkCapabilities caps = getNetworkCapabilities(getActiveNetwork()); if (caps != null) { return !caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED); } else { diff --git a/services/core/java/com/android/server/am/BroadcastFilter.java b/services/core/java/com/android/server/am/BroadcastFilter.java index 8e2ca0691277..34fca23ccb9e 100644 --- a/services/core/java/com/android/server/am/BroadcastFilter.java +++ b/services/core/java/com/android/server/am/BroadcastFilter.java @@ -81,7 +81,9 @@ final class BroadcastFilter extends IntentFilter { StringBuilder sb = new StringBuilder(); sb.append("BroadcastFilter{"); sb.append(Integer.toHexString(System.identityHashCode(this))); - sb.append(" u"); + sb.append(' '); + sb.append(owningUid); + sb.append("/u"); sb.append(owningUserId); sb.append(' '); sb.append(receiverList); diff --git a/services/core/java/com/android/server/am/BroadcastQueue.java b/services/core/java/com/android/server/am/BroadcastQueue.java index efb1c445925f..c1ed54e647d7 100644 --- a/services/core/java/com/android/server/am/BroadcastQueue.java +++ b/services/core/java/com/android/server/am/BroadcastQueue.java @@ -445,7 +445,7 @@ public final class BroadcastQueue { final long elapsed = finishTime - r.receiverTime; r.state = BroadcastRecord.IDLE; if (state == BroadcastRecord.IDLE) { - Slog.w(TAG, "finishReceiver [" + mQueueName + "] called but state is IDLE"); + Slog.w(TAG_BROADCAST, "finishReceiver [" + mQueueName + "] called but state is IDLE"); } if (r.allowBackgroundActivityStarts && r.curApp != null) { if (elapsed > mConstants.ALLOW_BG_ACTIVITY_START_TIMEOUT) { @@ -478,12 +478,13 @@ public final class BroadcastQueue { if (!r.timeoutExempt) { if (mConstants.SLOW_TIME > 0 && elapsed > mConstants.SLOW_TIME) { if (DEBUG_BROADCAST_DEFERRAL) { - Slog.i(TAG, "Broadcast receiver was slow: " + receiver + " br=" + r); + Slog.i(TAG_BROADCAST, "Broadcast receiver " + (r.nextReceiver - 1) + + " was slow: " + receiver + " br=" + r); } if (r.curApp != null) { mDispatcher.startDeferring(r.curApp.uid); } else { - Slog.d(TAG, "finish receiver curApp is null? " + r); + Slog.d(TAG_BROADCAST, "finish receiver curApp is null? " + r); } } } else { @@ -796,9 +797,7 @@ public final class BroadcastQueue { skipReceiverLocked(r); } } else { - if (r.receiverTime == 0) { - r.receiverTime = SystemClock.uptimeMillis(); - } + r.receiverTime = SystemClock.uptimeMillis(); maybeAddAllowBackgroundActivityStartsToken(filter.receiverList.app, r); performReceiveLocked(filter.receiverList.app, filter.receiverList.receiver, new Intent(r.intent), r.resultCode, r.resultData, @@ -1083,16 +1082,19 @@ public final class BroadcastQueue { if (newCount == 0) { // done! clear out this record's bookkeeping and deliver if (DEBUG_BROADCAST_DEFERRAL) { - Slog.i(TAG, "Sending broadcast completion for split token " - + r.splitToken); + Slog.i(TAG_BROADCAST, + "Sending broadcast completion for split token " + + r.splitToken + " : " + r.intent.getAction()); } mSplitRefcounts.delete(r.splitToken); } else { // still have some split broadcast records in flight; update refcount // and hold off on the callback if (DEBUG_BROADCAST_DEFERRAL) { - Slog.i(TAG, "Result refcount " + newCount + " for split token " - + r.splitToken + " - not sending completion yet"); + Slog.i(TAG_BROADCAST, + "Result refcount now " + newCount + " for split token " + + r.splitToken + " : " + r.intent.getAction() + + " - not sending completion yet"); } sendResult = false; mSplitRefcounts.put(r.splitToken, newCount); @@ -1155,7 +1157,7 @@ public final class BroadcastQueue { BroadcastRecord defer; if (r.nextReceiver + 1 == numReceivers) { if (DEBUG_BROADCAST_DEFERRAL) { - Slog.i(TAG, "Sole receiver of " + r + Slog.i(TAG_BROADCAST, "Sole receiver of " + r + " is under deferral; setting aside and proceeding"); } defer = r; @@ -1185,15 +1187,25 @@ public final class BroadcastQueue { // first split of this record; refcount for 'r' and 'deferred' r.splitToken = defer.splitToken = nextSplitTokenLocked(); mSplitRefcounts.put(r.splitToken, 2); + if (DEBUG_BROADCAST_DEFERRAL) { + Slog.i(TAG_BROADCAST, + "Broadcast needs split refcount; using new token " + + r.splitToken); + } } else { // new split from an already-refcounted situation; increment count final int curCount = mSplitRefcounts.get(token); if (DEBUG_BROADCAST_DEFERRAL) { if (curCount == 0) { - Slog.wtf(TAG, "Split refcount is zero with token for " + r); + Slog.wtf(TAG_BROADCAST, + "Split refcount is zero with token for " + r); } } mSplitRefcounts.put(token, curCount + 1); + if (DEBUG_BROADCAST_DEFERRAL) { + Slog.i(TAG_BROADCAST, "New split count for token " + token + + " is " + (curCount + 1)); + } } } } @@ -1529,7 +1541,7 @@ public final class BroadcastQueue { if (skip) { if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Skipping delivery of ordered [" + mQueueName + "] " - + r + " for whatever reason"); + + r + " for reason described above"); r.delivery[recIdx] = BroadcastRecord.DELIVERY_SKIPPED; r.receiver = null; r.curFilter = null; diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java index fa9b79d0b158..13525043412e 100644 --- a/services/core/java/com/android/server/am/BroadcastRecord.java +++ b/services/core/java/com/android/server/am/BroadcastRecord.java @@ -332,7 +332,6 @@ final class BroadcastRecord extends Binder { } splitReceivers.add(o); receivers.remove(i); - break; } else { i++; } @@ -350,6 +349,7 @@ final class BroadcastRecord extends Binder { resultData, resultExtras, ordered, sticky, initialSticky, userId, allowBackgroundActivityStarts, timeoutExempt); + split.splitToken = this.splitToken; return split; } diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java index 47b9c27284e5..1b14ce21bb92 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java @@ -53,6 +53,7 @@ import com.android.server.locksettings.recoverablekeystore.certificate.CertValid import com.android.server.locksettings.recoverablekeystore.certificate.CertXml; import com.android.server.locksettings.recoverablekeystore.certificate.SigXml; import com.android.server.locksettings.recoverablekeystore.storage.ApplicationKeyStorage; +import com.android.server.locksettings.recoverablekeystore.storage.CleanupManager; import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb; import com.android.server.locksettings.recoverablekeystore.storage.RecoverySessionStorage; import com.android.server.locksettings.recoverablekeystore.storage.RecoverySnapshotStorage; @@ -100,6 +101,7 @@ public class RecoverableKeyStoreManager { private final PlatformKeyManager mPlatformKeyManager; private final ApplicationKeyStorage mApplicationKeyStorage; private final TestOnlyInsecureCertificateHelper mTestCertHelper; + private final CleanupManager mCleanupManager; /** * Returns a new or existing instance. @@ -122,16 +124,24 @@ public class RecoverableKeyStoreManager { throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage()); } + RecoverySnapshotStorage snapshotStorage = + RecoverySnapshotStorage.newInstance(); + CleanupManager cleanupManager = CleanupManager.getInstance( + context.getApplicationContext(), + snapshotStorage, + db, + applicationKeyStorage); mInstance = new RecoverableKeyStoreManager( context.getApplicationContext(), db, new RecoverySessionStorage(), Executors.newSingleThreadExecutor(), - RecoverySnapshotStorage.newInstance(), + snapshotStorage, new RecoverySnapshotListenersStorage(), platformKeyManager, applicationKeyStorage, - new TestOnlyInsecureCertificateHelper()); + new TestOnlyInsecureCertificateHelper(), + cleanupManager); } return mInstance; } @@ -146,7 +156,8 @@ public class RecoverableKeyStoreManager { RecoverySnapshotListenersStorage listenersStorage, PlatformKeyManager platformKeyManager, ApplicationKeyStorage applicationKeyStorage, - TestOnlyInsecureCertificateHelper TestOnlyInsecureCertificateHelper) { + TestOnlyInsecureCertificateHelper testOnlyInsecureCertificateHelper, + CleanupManager cleanupManager) { mContext = context; mDatabase = recoverableKeyStoreDb; mRecoverySessionStorage = recoverySessionStorage; @@ -155,8 +166,10 @@ public class RecoverableKeyStoreManager { mSnapshotStorage = snapshotStorage; mPlatformKeyManager = platformKeyManager; mApplicationKeyStorage = applicationKeyStorage; - mTestCertHelper = TestOnlyInsecureCertificateHelper; - + mTestCertHelper = testOnlyInsecureCertificateHelper; + mCleanupManager = cleanupManager; + // Clears data for removed users. + mCleanupManager.verifyKnownUsers(); try { mRecoverableKeyGenerator = RecoverableKeyGenerator.newInstance(mDatabase); } catch (NoSuchAlgorithmException e) { @@ -955,6 +968,9 @@ public class RecoverableKeyStoreManager { mContext.enforceCallingOrSelfPermission( Manifest.permission.RECOVER_KEYSTORE, "Caller " + Binder.getCallingUid() + " doesn't have RecoverKeyStore permission."); + int userId = UserHandle.getCallingUserId(); + int uid = Binder.getCallingUid(); + mCleanupManager.registerRecoveryAgent(userId, uid); } private boolean publicKeysMatch(PublicKey publicKey, byte[] vaultParams) { diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/CleanupManager.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/CleanupManager.java new file mode 100644 index 000000000000..be35b50c361e --- /dev/null +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/CleanupManager.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.locksettings.recoverablekeystore.storage; + +import android.content.Context; +import android.os.ServiceSpecificException; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.locksettings.recoverablekeystore.WrappedKey; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Cleans up data when user is removed. + */ +public class CleanupManager { + private static final String TAG = "CleanupManager"; + + private final Context mContext; + private final UserManager mUserManager; + private final RecoverableKeyStoreDb mDatabase; + private final RecoverySnapshotStorage mSnapshotStorage; + private final ApplicationKeyStorage mApplicationKeyStorage; + + // Serial number can not be changed at runtime. + private Map<Integer, Long> mSerialNumbers; // Always in sync with the database. + + /** + * Creates a new instance of the class. + * IMPORTANT: {@code verifyKnownUsers} must be called before the first data access. + */ + public static CleanupManager getInstance( + Context context, + RecoverySnapshotStorage snapshotStorage, + RecoverableKeyStoreDb recoverableKeyStoreDb, + ApplicationKeyStorage applicationKeyStorage) { + return new CleanupManager( + context, + snapshotStorage, + recoverableKeyStoreDb, + UserManager.get(context), + applicationKeyStorage); + } + + @VisibleForTesting + CleanupManager( + Context context, + RecoverySnapshotStorage snapshotStorage, + RecoverableKeyStoreDb recoverableKeyStoreDb, + UserManager userManager, + ApplicationKeyStorage applicationKeyStorage) { + mContext = context; + mSnapshotStorage = snapshotStorage; + mDatabase = recoverableKeyStoreDb; + mUserManager = userManager; + mApplicationKeyStorage = applicationKeyStorage; + } + + /** + * Registers recovery agent in the system, if necessary. + */ + public synchronized void registerRecoveryAgent(int userId, int uid) { + if (mSerialNumbers == null) { + // Table was uninitialized. + verifyKnownUsers(); + } + // uid is ignored since recovery agent is a system app. + Long storedSerialNumber = mSerialNumbers.get(userId); + if (storedSerialNumber == null) { + storedSerialNumber = -1L; + } + if (storedSerialNumber != -1) { + // User was already registered. + return; + } + // User was added after {@code verifyAllUsers} call. + long currentSerialNumber = mUserManager.getSerialNumberForUser(UserHandle.of(userId)); + if (currentSerialNumber != -1) { + storeUserSerialNumber(userId, currentSerialNumber); + } + } + + /** + * Removes data if serial number for a user was changed. + */ + public synchronized void verifyKnownUsers() { + mSerialNumbers = mDatabase.getUserSerialNumbers(); + List<Integer> deletedUserIds = new ArrayList<Integer>(){}; + for (Map.Entry<Integer, Long> entry : mSerialNumbers.entrySet()) { + Integer userId = entry.getKey(); + Long storedSerialNumber = entry.getValue(); + if (storedSerialNumber == null) { + storedSerialNumber = -1L; + } + long currentSerialNumber = mUserManager.getSerialNumberForUser(UserHandle.of(userId)); + if (currentSerialNumber == -1) { + // User was removed. + deletedUserIds.add(userId); + removeDataForUser(userId); + } else if (storedSerialNumber == -1) { + // User is detected for the first time + storeUserSerialNumber(userId, currentSerialNumber); + } else if (storedSerialNumber != currentSerialNumber) { + // User has unexpected serial number - delete data related to old serial number. + deletedUserIds.add(userId); + removeDataForUser(userId); + // Register new user. + storeUserSerialNumber(userId, currentSerialNumber); + } + } + + for (Integer deletedUser : deletedUserIds) { + mSerialNumbers.remove(deletedUser); + } + } + + private void storeUserSerialNumber(int userId, long userSerialNumber) { + Log.d(TAG, "Storing serial number for user " + userId + "."); + mSerialNumbers.put(userId, userSerialNumber); + mDatabase.setUserSerialNumber(userId, userSerialNumber); + } + + /** + * Removes all data for given user, including + * + * <ul> + * <li> Recovery snapshots for all agents belonging to the {@code userId}. + * <li> Entries with data related to {@code userId} from the database. + * </ul> + */ + private void removeDataForUser(int userId) { + Log.d(TAG, "Removing data for user " + userId + "."); + List<Integer> recoveryAgents = mDatabase.getRecoveryAgents(userId); + for (Integer uid : recoveryAgents) { + mSnapshotStorage.remove(uid); + removeAllKeysForRecoveryAgent(userId, uid); + } + + mDatabase.removeUserFromAllTables(userId); + } + + /** + * Removes keys from Android KeyStore for the recovery agent; + * Doesn't remove encrypted key material from the database. + */ + private void removeAllKeysForRecoveryAgent(int userId, int uid) { + int generationId = mDatabase.getPlatformKeyGenerationId(userId); + Map<String, WrappedKey> allKeys = mDatabase.getAllKeys(userId, uid, generationId); + for (String alias : allKeys.keySet()) { + try { + // Delete KeyStore copy. + mApplicationKeyStorage.deleteEntry(userId, uid, alias); + } catch (ServiceSpecificException e) { + // Ignore errors during key removal. + Log.e(TAG, "Error while removing recoverable key " + alias + " : " + e); + } + } + } +} diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java index dffaffe677ad..3f5ac8e504b3 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java @@ -24,6 +24,7 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.security.keystore.recovery.RecoveryController; import android.text.TextUtils; +import android.util.ArrayMap; import android.util.Log; import com.android.server.locksettings.recoverablekeystore.TestOnlyInsecureCertificateHelper; @@ -261,7 +262,7 @@ public class RecoverableKeyStoreDb { * * @hide */ - public Map<String, WrappedKey> getAllKeys(int userId, int recoveryAgentUid, + public @NonNull Map<String, WrappedKey> getAllKeys(int userId, int recoveryAgentUid, int platformKeyGenerationId) { SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase(); String[] projection = { @@ -337,6 +338,58 @@ public class RecoverableKeyStoreDb { } /** + * Returns serial numbers associated with all known users. + * -1 is used for uninitialized serial numbers. + * + * See {@code UserHandle.getSerialNumberForUser}. + * @return Map from userId to serial numbers. + */ + public @NonNull Map<Integer, Long> getUserSerialNumbers() { + SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase(); + String[] projection = { + UserMetadataEntry.COLUMN_NAME_USER_ID, + UserMetadataEntry.COLUMN_NAME_USER_SERIAL_NUMBER}; + String selection = null; // get all rows. + String[] selectionArguments = {}; + + try ( + Cursor cursor = db.query( + UserMetadataEntry.TABLE_NAME, + projection, + selection, + selectionArguments, + /*groupBy=*/ null, + /*having=*/ null, + /*orderBy=*/ null) + ) { + Map<Integer, Long> serialNumbers = new ArrayMap<>(); + while (cursor.moveToNext()) { + int userId = cursor.getInt( + cursor.getColumnIndexOrThrow(UserMetadataEntry.COLUMN_NAME_USER_ID)); + long serialNumber = cursor.getLong(cursor.getColumnIndexOrThrow( + UserMetadataEntry.COLUMN_NAME_USER_SERIAL_NUMBER)); + serialNumbers.put(userId, serialNumber); + } + return serialNumbers; + } + } + + /** + * Sets the {@code serialNumber} for the user {@code userId}. + * + * @return The primary key of the inserted row, or -1 if failed. + */ + public long setUserSerialNumber(int userId, long serialNumber) { + SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(UserMetadataEntry.COLUMN_NAME_USER_ID, userId); + values.put(UserMetadataEntry.COLUMN_NAME_USER_SERIAL_NUMBER, serialNumber); + long result = db.replace( + UserMetadataEntry.TABLE_NAME, /*nullColumnHack=*/ null, values); + return result; + } + + /** * Updates status of old keys to {@code RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE}. */ public void invalidateKeysWithOldGenerationId(int userId, int newGenerationId) { @@ -424,8 +477,7 @@ public class RecoverableKeyStoreDb { */ @Nullable public Long getRecoveryServiceCertSerial(int userId, int uid, @NonNull String rootAlias) { - return getLong(userId, uid, rootAlias, - RecoveryServiceMetadataEntry.COLUMN_NAME_CERT_SERIAL); + return getLong(userId, uid, rootAlias, RootOfTrustEntry.COLUMN_NAME_CERT_SERIAL); } /** @@ -441,7 +493,7 @@ public class RecoverableKeyStoreDb { */ public long setRecoveryServiceCertSerial(int userId, int uid, @NonNull String rootAlias, long serial) { - return setLong(userId, uid, rootAlias, RecoveryServiceMetadataEntry.COLUMN_NAME_CERT_SERIAL, + return setLong(userId, uid, rootAlias, RootOfTrustEntry.COLUMN_NAME_CERT_SERIAL, serial); } @@ -457,8 +509,7 @@ public class RecoverableKeyStoreDb { */ @Nullable public CertPath getRecoveryServiceCertPath(int userId, int uid, @NonNull String rootAlias) { - byte[] bytes = getBytes(userId, uid, rootAlias, - RecoveryServiceMetadataEntry.COLUMN_NAME_CERT_PATH); + byte[] bytes = getBytes(userId, uid, rootAlias, RootOfTrustEntry.COLUMN_NAME_CERT_PATH); if (bytes == null) { return null; } @@ -489,7 +540,7 @@ public class RecoverableKeyStoreDb { if (certPath.getCertificates().size() == 0) { throw new CertificateEncodingException("No certificate contained in the cert path."); } - return setBytes(userId, uid, rootAlias, RecoveryServiceMetadataEntry.COLUMN_NAME_CERT_PATH, + return setBytes(userId, uid, rootAlias, RootOfTrustEntry.COLUMN_NAME_CERT_PATH, certPath.getEncoded(CERT_PATH_ENCODING)); } @@ -1189,6 +1240,63 @@ public class RecoverableKeyStoreDb { RootOfTrustEntry.TABLE_NAME, values, selection, selectionArguments); } + /** + * Removes all entries for given {@code userId}. + */ + public void removeUserFromAllTables(int userId) { + removeUserFromKeysTable(userId); + removeUserFromUserMetadataTable(userId); + removeUserFromRecoveryServiceMetadataTable(userId); + removeUserFromRootOfTrustTable(userId); + } + + /** + * Removes all entries for given userId from Keys table. + * + * @return {@code true} if deleted a row. + */ + private boolean removeUserFromKeysTable(int userId) { + SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase(); + String selection = KeysEntry.COLUMN_NAME_USER_ID + " = ?"; + String[] selectionArgs = {Integer.toString(userId)}; + return db.delete(KeysEntry.TABLE_NAME, selection, selectionArgs) > 0; + } + + /** + * Removes all entries for given userId from UserMetadata table. + * + * @return {@code true} if deleted a row. + */ + private boolean removeUserFromUserMetadataTable(int userId) { + SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase(); + String selection = UserMetadataEntry.COLUMN_NAME_USER_ID + " = ?"; + String[] selectionArgs = {Integer.toString(userId)}; + return db.delete(UserMetadataEntry.TABLE_NAME, selection, selectionArgs) > 0; + } + + /** + * Removes all entries for given userId from RecoveryServiceMetadata table. + * + * @return {@code true} if deleted a row. + */ + private boolean removeUserFromRecoveryServiceMetadataTable(int userId) { + SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase(); + String selection = RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + " = ?"; + String[] selectionArgs = {Integer.toString(userId)}; + return db.delete(RecoveryServiceMetadataEntry.TABLE_NAME, selection, selectionArgs) > 0; + } + + /** + * Removes all entries for given userId from RootOfTrust table. + * + * @return {@code true} if deleted a row. + */ + private boolean removeUserFromRootOfTrustTable(int userId) { + SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase(); + String selection = RootOfTrustEntry.COLUMN_NAME_USER_ID + " = ?"; + String[] selectionArgs = {Integer.toString(userId)}; + return db.delete(RootOfTrustEntry.TABLE_NAME, selection, selectionArgs) > 0; + } /** * Creates an empty row in the recovery service metadata table if such a row doesn't exist for diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java index b58ee4bc9d74..e79d11732dd9 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java @@ -20,6 +20,8 @@ import android.provider.BaseColumns; /** * Contract for recoverable key database. Describes the tables present. + * + * Make sure that {@code removeUserFromAllKnownTables} is updated, when new table is added. */ class RecoverableKeyStoreDbContract { /** @@ -91,6 +93,11 @@ class RecoverableKeyStoreDbContract { * is used to wrap recoverable keys on disk. */ static final String COLUMN_NAME_PLATFORM_KEY_GENERATION_ID = "platform_key_generation_id"; + + /** + * Serial number for the user which can not be reused. Default value is {@code -1}. + */ + static final String COLUMN_NAME_USER_SERIAL_NUMBER = "user_serial_number"; } /** diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java index b0613da35d28..cd5e8cf65a2d 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java @@ -32,7 +32,7 @@ import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKe class RecoverableKeyStoreDbHelper extends SQLiteOpenHelper { private static final String TAG = "RecoverableKeyStoreDbHp"; - static final int DATABASE_VERSION = 5; + static final int DATABASE_VERSION = 6; // Added user id serial number. private static final String DATABASE_NAME = "recoverablekeystore.db"; private static final String SQL_CREATE_KEYS_ENTRY = @@ -54,7 +54,8 @@ class RecoverableKeyStoreDbHelper extends SQLiteOpenHelper { "CREATE TABLE " + UserMetadataEntry.TABLE_NAME + "( " + UserMetadataEntry._ID + " INTEGER PRIMARY KEY," + UserMetadataEntry.COLUMN_NAME_USER_ID + " INTEGER UNIQUE," - + UserMetadataEntry.COLUMN_NAME_PLATFORM_KEY_GENERATION_ID + " INTEGER)"; + + UserMetadataEntry.COLUMN_NAME_PLATFORM_KEY_GENERATION_ID + " INTEGER," + + UserMetadataEntry.COLUMN_NAME_USER_SERIAL_NUMBER + " INTEGER DEFAULT -1)"; private static final String SQL_CREATE_RECOVERY_SERVICE_METADATA_ENTRY = "CREATE TABLE " + RecoveryServiceMetadataEntry.TABLE_NAME + " (" @@ -141,6 +142,11 @@ class RecoverableKeyStoreDbHelper extends SQLiteOpenHelper { oldVersion = 5; } + if (oldVersion < 6 && newVersion >= 6) { + upgradeDbForVersion6(db); + oldVersion = 6; + } + if (oldVersion != newVersion) { Log.e(TAG, "Failed to update recoverablekeystore database to the most recent version"); } @@ -179,6 +185,15 @@ class RecoverableKeyStoreDbHelper extends SQLiteOpenHelper { KeysEntry.COLUMN_NAME_KEY_METADATA, "BLOB", /*defaultStr=*/ null); } + private void upgradeDbForVersion6(SQLiteDatabase db) { + Log.d(TAG, "Updating recoverable keystore database to version 6"); + // adds a column to store the user serial number + addColumnToTable(db, UserMetadataEntry.TABLE_NAME, + UserMetadataEntry.COLUMN_NAME_USER_SERIAL_NUMBER, + "INTEGER DEFAULT -1", + /*defaultStr=*/ null); + } + private static void addColumnToTable( SQLiteDatabase db, String tableName, String column, String columnType, String defaultStr) { diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index 9e9128430e01..05af13ab9eec 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -744,12 +744,8 @@ public class LauncherAppsService extends SystemService { if (!canAccessProfile(user.getIdentifier(), "Cannot access usage limit")) { return null; } - - final PackageManagerInternal pmi = - LocalServices.getService(PackageManagerInternal.class); - final ComponentName cn = pmi.getDefaultHomeActivity(user.getIdentifier()); - if (!cn.getPackageName().equals(callingPackage)) { - throw new SecurityException("Caller is not the active launcher"); + if (!mActivityTaskManagerInternal.isCallerRecents(Binder.getCallingUid())) { + throw new SecurityException("Caller is not the recents app"); } final UsageStatsManagerInternal.AppUsageLimitData data = diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index b72e83692e8a..a3b72fd02654 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -481,6 +481,12 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements } } + if (callingUid == Process.SYSTEM_UID) { + params.installFlags |= PackageManager.INSTALL_RESPECT_ALLOW_DOWNGRADE; + } else { + params.installFlags &= ~PackageManager.INSTALL_RESPECT_ALLOW_DOWNGRADE; + } + boolean isApex = (params.installFlags & PackageManager.INSTALL_APEX) != 0; if (params.isStaged || isApex) { mContext.enforceCallingOrSelfPermission(Manifest.permission.INSTALL_PACKAGES, TAG); diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java index 3218c8608d77..ff81ad56f45f 100644 --- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java +++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java @@ -806,7 +806,7 @@ public class PackageManagerServiceUtils { */ public static boolean isDowngradePermitted(int installFlags, int applicationFlags) { // If installed, the package will get access to data left on the device by its - // predecessor. As a security measure, this is permited only if this is not a + // predecessor. As a security measure, this is permitted only if this is not a // version downgrade or if the predecessor package is marked as debuggable and // a downgrade is explicitly requested. // @@ -818,12 +818,21 @@ public class PackageManagerServiceUtils { // installFlags. This is because we aim to keep the behavior of debuggable // platform builds as close as possible to the behavior of non-debuggable // platform builds. + // + // In case of user builds, downgrade is permitted only for the system server initiated + // sessions. This is enforced by INSTALL_RESPECT_ALLOW_DOWNGRADE flag parameter. final boolean downgradeRequested = (installFlags & PackageManager.INSTALL_ALLOW_DOWNGRADE) != 0; - final boolean packageDebuggable = - (applicationFlags - & ApplicationInfo.FLAG_DEBUGGABLE) != 0; - return (downgradeRequested) && ((Build.IS_DEBUGGABLE) || (packageDebuggable)); + if (!downgradeRequested) { + return false; + } + final boolean isDebuggable = + Build.IS_DEBUGGABLE || ((applicationFlags + & ApplicationInfo.FLAG_DEBUGGABLE) != 0); + if (isDebuggable) { + return true; + } + return (installFlags & PackageManager.INSTALL_RESPECT_ALLOW_DOWNGRADE) != 0; } /** diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java index c78b96d2d294..5bab65c8b642 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java @@ -59,6 +59,7 @@ import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import com.android.server.locksettings.recoverablekeystore.storage.ApplicationKeyStorage; +import com.android.server.locksettings.recoverablekeystore.storage.CleanupManager; import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb; import com.android.server.locksettings.recoverablekeystore.storage.RecoverySessionStorage; import com.android.server.locksettings.recoverablekeystore.storage.RecoverySnapshotStorage; @@ -154,6 +155,7 @@ public class RecoverableKeyStoreManagerTest { @Mock private KeyguardManager mKeyguardManager; @Mock private PlatformKeyManager mPlatformKeyManager; @Mock private ApplicationKeyStorage mApplicationKeyStorage; + @Mock private CleanupManager mCleanupManager; @Spy private TestOnlyInsecureCertificateHelper mTestOnlyInsecureCertificateHelper; private RecoverableKeyStoreDb mRecoverableKeyStoreDb; @@ -191,7 +193,8 @@ public class RecoverableKeyStoreManagerTest { mMockListenersStorage, mPlatformKeyManager, mApplicationKeyStorage, - mTestOnlyInsecureCertificateHelper); + mTestOnlyInsecureCertificateHelper, + mCleanupManager); } @After diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/CleanupManagerTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/CleanupManagerTest.java new file mode 100644 index 000000000000..0b15a126e98a --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/CleanupManagerTest.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.locksettings.recoverablekeystore.storage; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.os.UserHandle; +import android.os.UserManager; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class CleanupManagerTest { + private static final int USER_ID = 10; + private static final int USER_ID_2 = 20; + private static final int UID = 1234; + private static final long USER_SERIAL_NUMBER = 101L; + private static final long USER_SERIAL_NUMBER_2 = 202L; + + private Context mContext; + private CleanupManager mManager; + + @Mock private RecoverableKeyStoreDb mDatabase; + @Mock private RecoverySnapshotStorage mRecoverySnapshotStorage; + @Mock private UserManager mUserManager; + @Mock private ApplicationKeyStorage mApplicationKeyStorage; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mContext = InstrumentationRegistry.getTargetContext(); + mManager = new CleanupManager(mContext, mRecoverySnapshotStorage, mDatabase, mUserManager, + mApplicationKeyStorage); + } + + @Test + public void registerRecoveryAgent_unknownUser_storesInDb() throws Exception { + when(mDatabase.getUserSerialNumbers()).thenReturn(new HashMap<>()); + when(mUserManager.getSerialNumberForUser(eq(UserHandle.of(USER_ID)))) + .thenReturn(USER_SERIAL_NUMBER); + when(mUserManager.getSerialNumberForUser(eq(UserHandle.of(USER_ID_2)))) + .thenReturn(USER_SERIAL_NUMBER_2); + + mManager.registerRecoveryAgent(USER_ID, UID); + mManager.registerRecoveryAgent(USER_ID_2, UID); + + verify(mDatabase).setUserSerialNumber(USER_ID, USER_SERIAL_NUMBER); + verify(mDatabase).setUserSerialNumber(USER_ID_2, USER_SERIAL_NUMBER_2); + + } + + @Test + public void registerRecoveryAgent_registersSameUser_doesntChangeDb() throws Exception { + when(mDatabase.getUserSerialNumbers()).thenReturn(new HashMap<>()); + when(mUserManager.getSerialNumberForUser(eq(UserHandle.of(USER_ID)))) + .thenReturn(USER_SERIAL_NUMBER); + + mManager.registerRecoveryAgent(USER_ID, UID); + mManager.registerRecoveryAgent(USER_ID, UID); // ignored. + + verify(mDatabase, times(1)).setUserSerialNumber(USER_ID, USER_SERIAL_NUMBER); + } + + @Test + public void verifyKnownUsers_newSerialNumber_deletesData() throws Exception { + Map knownSerialNumbers = new HashMap<>(); + knownSerialNumbers.put(USER_ID, USER_SERIAL_NUMBER); + when(mDatabase.getUserSerialNumbers()).thenReturn(knownSerialNumbers); + List<Integer> recoveryAgents = new ArrayList<>(); + recoveryAgents.add(UID); + when(mDatabase.getRecoveryAgents(USER_ID)).thenReturn(recoveryAgents); + + when(mUserManager.getSerialNumberForUser(eq(UserHandle.of(USER_ID)))) + .thenReturn(USER_SERIAL_NUMBER_2); // new value + + + mManager.verifyKnownUsers(); + + verify(mDatabase).removeUserFromAllTables(USER_ID); + verify(mDatabase).setUserSerialNumber(USER_ID, USER_SERIAL_NUMBER_2); + verify(mRecoverySnapshotStorage).remove(UID); + } +} + diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelperTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelperTest.java index 35215c34d8f0..2658af68f78b 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelperTest.java @@ -51,6 +51,7 @@ public class RecoverableKeyStoreDbHelperTest { private static final long TEST_LAST_SYNCED_AT = 1517990732000L; private static final int TEST_RECOVERY_STATUS = 3; private static final int TEST_PLATFORM_KEY_GENERATION_ID = 11; + private static final int TEST_USER_SERIAL_NUMBER = 15; private static final int TEST_SNAPSHOT_VERSION = 31; private static final int TEST_SHOULD_CREATE_SNAPSHOT = 1; private static final byte[] TEST_PUBLIC_KEY = "test-public-key".getBytes(UTF_8); @@ -234,5 +235,14 @@ public class RecoverableKeyStoreDbHelperTest { assertThat(mDatabase.replace(KeysEntry.TABLE_NAME, /*nullColumnHack=*/ null, values)) .isGreaterThan(-1L); + + // User serial number column was added when upgrading from v5 to v6 + values = new ContentValues(); + values.put(UserMetadataEntry.COLUMN_NAME_USER_ID, TEST_USER_ID); + values.put(UserMetadataEntry.COLUMN_NAME_USER_SERIAL_NUMBER, TEST_USER_SERIAL_NUMBER); + assertThat( + mDatabase.replace(UserMetadataEntry.TABLE_NAME, /*nullColumnHack=*/ null, values)) + .isGreaterThan(-1L); } + } diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbTest.java index 7de9ffc7f3cc..932a769c86bc 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbTest.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbTest.java @@ -279,6 +279,55 @@ public class RecoverableKeyStoreDbTest { } @Test + public void getUserSerialNumbers_returnsSerialNumbers() { + int userId = 42; + int userId2 = 44; + Long serialNumber = 24L; + Long serialNumber2 = 25L; + mRecoverableKeyStoreDb.setUserSerialNumber(userId, serialNumber); + mRecoverableKeyStoreDb.setUserSerialNumber(userId2, serialNumber2); + + assertEquals(2, mRecoverableKeyStoreDb.getUserSerialNumbers().size()); + assertEquals(serialNumber, mRecoverableKeyStoreDb.getUserSerialNumbers().get(userId)); + assertEquals(serialNumber2, mRecoverableKeyStoreDb.getUserSerialNumbers().get(userId2)); + } + + @Test + public void getUserSerialNumbers_returnsMinusOneIfNoEntry() { + int userId = 42; + int generationId = 24; + Long serialNumber = -1L; + // Don't set serial number + mRecoverableKeyStoreDb.setPlatformKeyGenerationId(userId, generationId); + + assertEquals(1, mRecoverableKeyStoreDb.getUserSerialNumbers().size()); + assertEquals(serialNumber, mRecoverableKeyStoreDb.getUserSerialNumbers().get(userId)); + } + + @Test + public void removeUserFromAllTables_removesData() throws Exception { + int userId = 12; + int generationId = 24; + int[] types = new int[]{1}; + int uid = 10009; + mRecoverableKeyStoreDb.setRecoveryServiceCertSerial(userId, uid, + TEST_ROOT_CERT_ALIAS, 1234L); + mRecoverableKeyStoreDb.setPlatformKeyGenerationId(userId, generationId); + mRecoverableKeyStoreDb.setActiveRootOfTrust(userId, uid, "root"); + mRecoverableKeyStoreDb.setRecoverySecretTypes(userId, uid, types); + + mRecoverableKeyStoreDb.removeUserFromAllTables(userId); + + // RootOfTrust + assertThat(mRecoverableKeyStoreDb.getRecoveryServiceCertSerial(userId, uid, + TEST_ROOT_CERT_ALIAS)).isNull(); + // UserMetadata + assertThat(mRecoverableKeyStoreDb.getPlatformKeyGenerationId(userId)).isEqualTo(-1); + // RecoveryServiceMetadata + assertThat(mRecoverableKeyStoreDb.getRecoverySecretTypes(userId, uid)).isEmpty(); + } + + @Test public void setRecoveryStatus_withSingleKey() { int userId = 12; int uid = 1009; diff --git a/telephony/java/android/telephony/NetworkRegistrationState.java b/telephony/java/android/telephony/NetworkRegistrationState.java index 9614dc52324d..a9d307953ced 100644 --- a/telephony/java/android/telephony/NetworkRegistrationState.java +++ b/telephony/java/android/telephony/NetworkRegistrationState.java @@ -285,6 +285,14 @@ public class NetworkRegistrationState implements Parcelable { } /** + * @hide + * @return {@code true} if in service. + */ + public boolean isInService() { + return mRegState == REG_STATE_HOME || mRegState == REG_STATE_ROAMING; + } + + /** * Set {@link ServiceState.RoamingType roaming type}. This could override * roaming type based on resource overlay or carrier config. * @hide diff --git a/tests/net/java/com/android/server/ConnectivityServiceTest.java b/tests/net/java/com/android/server/ConnectivityServiceTest.java index 1a0e8fa490b3..fbc1a651941b 100644 --- a/tests/net/java/com/android/server/ConnectivityServiceTest.java +++ b/tests/net/java/com/android/server/ConnectivityServiceTest.java @@ -939,11 +939,19 @@ public class ConnectivityServiceTest { return mConnected; // Similar trickery } - public void connect() { + private void connect(boolean isAlwaysMetered) { mNetworkCapabilities.set(mMockNetworkAgent.getNetworkCapabilities()); mConnected = true; mConfig = new VpnConfig(); - mConfig.isMetered = false; + mConfig.isMetered = isAlwaysMetered; + } + + public void connectAsAlwaysMetered() { + connect(true /* isAlwaysMetered */); + } + + public void connect() { + connect(false /* isAlwaysMetered */); } @Override @@ -5104,6 +5112,202 @@ public class ConnectivityServiceTest { } @Test + public void testIsActiveNetworkMeteredOverWifi() { + // Returns true by default when no network is available. + assertTrue(mCm.isActiveNetworkMetered()); + mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI); + mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + mWiFiNetworkAgent.connect(true); + waitForIdle(); + + assertFalse(mCm.isActiveNetworkMetered()); + } + + @Test + public void testIsActiveNetworkMeteredOverCell() { + // Returns true by default when no network is available. + assertTrue(mCm.isActiveNetworkMetered()); + mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR); + mCellNetworkAgent.removeCapability(NET_CAPABILITY_NOT_METERED); + mCellNetworkAgent.connect(true); + waitForIdle(); + + assertTrue(mCm.isActiveNetworkMetered()); + } + + @Test + public void testIsActiveNetworkMeteredOverVpnTrackingPlatformDefault() { + // Returns true by default when no network is available. + assertTrue(mCm.isActiveNetworkMetered()); + mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR); + mCellNetworkAgent.removeCapability(NET_CAPABILITY_NOT_METERED); + mCellNetworkAgent.connect(true); + waitForIdle(); + assertTrue(mCm.isActiveNetworkMetered()); + + // Connect VPN network. By default it is using current default network (Cell). + MockNetworkAgent vpnNetworkAgent = new MockNetworkAgent(TRANSPORT_VPN); + final ArraySet<UidRange> ranges = new ArraySet<>(); + final int uid = Process.myUid(); + ranges.add(new UidRange(uid, uid)); + mMockVpn.setNetworkAgent(vpnNetworkAgent); + mMockVpn.setUids(ranges); + vpnNetworkAgent.connect(true); + mMockVpn.connect(); + waitForIdle(); + // Ensure VPN is now the active network. + assertEquals(vpnNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + + // Expect VPN to be metered. + assertTrue(mCm.isActiveNetworkMetered()); + + // Connect WiFi. + mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI); + mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + mWiFiNetworkAgent.connect(true); + waitForIdle(); + // VPN should still be the active network. + assertEquals(vpnNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + + // Expect VPN to be unmetered as it should now be using WiFi (new default). + assertFalse(mCm.isActiveNetworkMetered()); + + // Disconnecting Cell should not affect VPN's meteredness. + mCellNetworkAgent.disconnect(); + waitForIdle(); + + assertFalse(mCm.isActiveNetworkMetered()); + + // Disconnect WiFi; Now there is no platform default network. + mWiFiNetworkAgent.disconnect(); + waitForIdle(); + + // VPN without any underlying networks is treated as metered. + assertTrue(mCm.isActiveNetworkMetered()); + + vpnNetworkAgent.disconnect(); + mMockVpn.disconnect(); + } + + @Test + public void testIsActiveNetworkMeteredOverVpnSpecifyingUnderlyingNetworks() { + // Returns true by default when no network is available. + assertTrue(mCm.isActiveNetworkMetered()); + mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR); + mCellNetworkAgent.removeCapability(NET_CAPABILITY_NOT_METERED); + mCellNetworkAgent.connect(true); + waitForIdle(); + assertTrue(mCm.isActiveNetworkMetered()); + + mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI); + mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + mWiFiNetworkAgent.connect(true); + waitForIdle(); + assertFalse(mCm.isActiveNetworkMetered()); + + // Connect VPN network. + MockNetworkAgent vpnNetworkAgent = new MockNetworkAgent(TRANSPORT_VPN); + final ArraySet<UidRange> ranges = new ArraySet<>(); + final int uid = Process.myUid(); + ranges.add(new UidRange(uid, uid)); + mMockVpn.setNetworkAgent(vpnNetworkAgent); + mMockVpn.setUids(ranges); + vpnNetworkAgent.connect(true); + mMockVpn.connect(); + waitForIdle(); + // Ensure VPN is now the active network. + assertEquals(vpnNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + // VPN is using Cell + mService.setUnderlyingNetworksForVpn( + new Network[] { mCellNetworkAgent.getNetwork() }); + waitForIdle(); + + // Expect VPN to be metered. + assertTrue(mCm.isActiveNetworkMetered()); + + // VPN is now using WiFi + mService.setUnderlyingNetworksForVpn( + new Network[] { mWiFiNetworkAgent.getNetwork() }); + waitForIdle(); + + // Expect VPN to be unmetered + assertFalse(mCm.isActiveNetworkMetered()); + + // VPN is using Cell | WiFi. + mService.setUnderlyingNetworksForVpn( + new Network[] { mCellNetworkAgent.getNetwork(), mWiFiNetworkAgent.getNetwork() }); + waitForIdle(); + + // Expect VPN to be metered. + assertTrue(mCm.isActiveNetworkMetered()); + + // VPN is using WiFi | Cell. + mService.setUnderlyingNetworksForVpn( + new Network[] { mWiFiNetworkAgent.getNetwork(), mCellNetworkAgent.getNetwork() }); + waitForIdle(); + + // Order should not matter and VPN should still be metered. + assertTrue(mCm.isActiveNetworkMetered()); + + // VPN is not using any underlying networks. + mService.setUnderlyingNetworksForVpn(new Network[0]); + waitForIdle(); + + // VPN without underlying networks is treated as metered. + assertTrue(mCm.isActiveNetworkMetered()); + + vpnNetworkAgent.disconnect(); + mMockVpn.disconnect(); + } + + @Test + public void testIsActiveNetworkMeteredOverAlwaysMeteredVpn() { + // Returns true by default when no network is available. + assertTrue(mCm.isActiveNetworkMetered()); + mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI); + mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + mWiFiNetworkAgent.connect(true); + waitForIdle(); + assertFalse(mCm.isActiveNetworkMetered()); + + // Connect VPN network. + MockNetworkAgent vpnNetworkAgent = new MockNetworkAgent(TRANSPORT_VPN); + final ArraySet<UidRange> ranges = new ArraySet<>(); + final int uid = Process.myUid(); + ranges.add(new UidRange(uid, uid)); + mMockVpn.setNetworkAgent(vpnNetworkAgent); + mMockVpn.setUids(ranges); + vpnNetworkAgent.connect(true); + mMockVpn.connectAsAlwaysMetered(); + waitForIdle(); + assertEquals(vpnNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + + // VPN is tracking current platform default (WiFi). + mService.setUnderlyingNetworksForVpn(null); + waitForIdle(); + + // Despite VPN using WiFi (which is unmetered), VPN itself is marked as always metered. + assertTrue(mCm.isActiveNetworkMetered()); + + // VPN explicitly declares WiFi as its underlying network. + mService.setUnderlyingNetworksForVpn( + new Network[] { mWiFiNetworkAgent.getNetwork() }); + waitForIdle(); + + // Doesn't really matter whether VPN declares its underlying networks explicitly. + assertTrue(mCm.isActiveNetworkMetered()); + + // With WiFi lost, VPN is basically without any underlying networks. And in that case it is + // anyways suppose to be metered. + mWiFiNetworkAgent.disconnect(); + waitForIdle(); + + assertTrue(mCm.isActiveNetworkMetered()); + + vpnNetworkAgent.disconnect(); + } + + @Test public void testNetworkBlockedStatus() { final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback(); final NetworkRequest cellRequest = new NetworkRequest.Builder() |