diff options
39 files changed, 1189 insertions, 114 deletions
diff --git a/apct-tests/perftests/core/OWNERS b/apct-tests/perftests/core/OWNERS new file mode 100644 index 000000000000..18486af9d12c --- /dev/null +++ b/apct-tests/perftests/core/OWNERS @@ -0,0 +1 @@ +include /graphics/java/android/graphics/fonts/OWNERS diff --git a/core/java/android/view/FrameMetrics.java b/core/java/android/view/FrameMetrics.java index c2566cc78bb0..5937499744b8 100644 --- a/core/java/android/view/FrameMetrics.java +++ b/core/java/android/view/FrameMetrics.java @@ -149,7 +149,7 @@ public final class FrameMetrics { * <p> * The time value that was used in all the vsync listeners and drawing for * the frame (Choreographer frame callbacks, animations, - * {@link View#getDrawingTime()}, etc…) + * {@link View#getDrawingTime()}, etc.) * </p> */ public static final int VSYNC_TIMESTAMP = 11; diff --git a/core/java/com/android/internal/os/Zygote.java b/core/java/com/android/internal/os/Zygote.java index 92fe72146999..d196d4af2e42 100644 --- a/core/java/com/android/internal/os/Zygote.java +++ b/core/java/com/android/internal/os/Zygote.java @@ -162,7 +162,7 @@ public final class Zygote { * GWP-ASan is activated unconditionally (but still, only a small subset of * allocations is protected). */ - public static final int GWP_ASAN_LEVEL_ALWAYS = 2 << 21; + public static final int GWP_ASAN_LEVEL_ALWAYS = 1 << 22; /** * Enable automatic zero-initialization of native heap memory allocations. diff --git a/core/proto/OWNERS b/core/proto/OWNERS index 748b4b4f5743..99fd21592411 100644 --- a/core/proto/OWNERS +++ b/core/proto/OWNERS @@ -16,6 +16,7 @@ ogunwale@google.com jjaggi@google.com roosa@google.com per-file usagestatsservice.proto, usagestatsservice_v2.proto = mwachens@google.com +per-file apphibernationservice.proto = file:/core/java/android/apphibernation/OWNERS # Biometrics kchyn@google.com diff --git a/core/proto/android/server/apphibernationservice.proto b/core/proto/android/server/apphibernationservice.proto new file mode 100644 index 000000000000..d341c4b2f0a8 --- /dev/null +++ b/core/proto/android/server/apphibernationservice.proto @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2021 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. + */ + +syntax = "proto2"; +package com.android.server.apphibernation; + +option java_multiple_files = true; + +// Proto for hibernation states for all packages for a user. +message UserLevelHibernationStatesProto { + repeated UserLevelHibernationStateProto hibernation_state = 1; +} + +// Proto for com.android.server.apphibernation.UserLevelState. +message UserLevelHibernationStateProto { + optional string package_name = 1; + optional bool hibernated = 2; +} + +// Proto for global hibernation states for all packages. +message GlobalLevelHibernationStatesProto { + repeated GlobalLevelHibernationStateProto hibernation_state = 1; +} + +// Proto for com.android.server.apphibernation.GlobalLevelState +message GlobalLevelHibernationStateProto { + optional string package_name = 1; + optional bool hibernated = 2; +}
\ No newline at end of file diff --git a/core/tests/coretests/assets/fonts/OWNERS b/core/tests/coretests/assets/fonts/OWNERS new file mode 100644 index 000000000000..b0e0b9deaddb --- /dev/null +++ b/core/tests/coretests/assets/fonts/OWNERS @@ -0,0 +1 @@ +include /core/java/android/text/OWNERS diff --git a/core/tests/coretests/assets/fonts_for_family_selection/OWNERS b/core/tests/coretests/assets/fonts_for_family_selection/OWNERS new file mode 100644 index 000000000000..b0e0b9deaddb --- /dev/null +++ b/core/tests/coretests/assets/fonts_for_family_selection/OWNERS @@ -0,0 +1 @@ +include /core/java/android/text/OWNERS diff --git a/core/tests/coretests/res/font/OWNERS b/core/tests/coretests/res/font/OWNERS new file mode 100644 index 000000000000..b0e0b9deaddb --- /dev/null +++ b/core/tests/coretests/res/font/OWNERS @@ -0,0 +1 @@ +include /core/java/android/text/OWNERS diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 7538c8b7ffad..2b53257e2774 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -771,7 +771,8 @@ public class BubbleController { // if the bubble is already active, there's no need to push it to overflow return; } - bubble.inflate((b) -> mBubbleData.overflowBubble(DISMISS_AGED, bubble), + bubble.inflate( + (b) -> mBubbleData.overflowBubble(Bubbles.DISMISS_RELOAD_FROM_DISK, bubble), mContext, this, mStackView, mBubbleIconFactory, true /* skipInflation */); }); return null; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java index 9d196ba32f8a..53b75373a647 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java @@ -542,7 +542,8 @@ public class BubbleData { void overflowBubble(@DismissReason int reason, Bubble bubble) { if (bubble.getPendingIntentCanceled() || !(reason == Bubbles.DISMISS_AGED - || reason == Bubbles.DISMISS_USER_GESTURE)) { + || reason == Bubbles.DISMISS_USER_GESTURE + || reason == Bubbles.DISMISS_RELOAD_FROM_DISK)) { return; } if (DEBUG_BUBBLE_DATA) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java index 3361c4ce11da..c88a58be1461 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java @@ -61,7 +61,10 @@ public class BubbleLogger { BUBBLE_OVERFLOW_REMOVE_BLOCKED(490), @UiEvent(doc = "User selected the overflow.") - BUBBLE_OVERFLOW_SELECTED(600); + BUBBLE_OVERFLOW_SELECTED(600), + + @UiEvent(doc = "Restore bubble to overflow after phone reboot.") + BUBBLE_OVERFLOW_RECOVER(691); private final int mId; @@ -112,6 +115,8 @@ public class BubbleLogger { log(b, Event.BUBBLE_OVERFLOW_ADD_AGED); } else if (r == Bubbles.DISMISS_USER_GESTURE) { log(b, Event.BUBBLE_OVERFLOW_ADD_USER_GESTURE); + } else if (r == Bubbles.DISMISS_RELOAD_FROM_DISK) { + log(b, Event.BUBBLE_OVERFLOW_RECOVER); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java index 6102147e4b53..6a1026bb24fe 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java @@ -54,7 +54,7 @@ public interface Bubbles { DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE, DISMISS_USER_CHANGED, DISMISS_GROUP_CANCELLED, DISMISS_INVALID_INTENT, DISMISS_OVERFLOW_MAX_REACHED, DISMISS_SHORTCUT_REMOVED, DISMISS_PACKAGE_REMOVED, - DISMISS_NO_BUBBLE_UP}) + DISMISS_NO_BUBBLE_UP, DISMISS_RELOAD_FROM_DISK}) @Target({FIELD, LOCAL_VARIABLE, PARAMETER}) @interface DismissReason {} @@ -72,6 +72,7 @@ public interface Bubbles { int DISMISS_SHORTCUT_REMOVED = 12; int DISMISS_PACKAGE_REMOVED = 13; int DISMISS_NO_BUBBLE_UP = 14; + int DISMISS_RELOAD_FROM_DISK = 15; /** * @return {@code true} if there is a bubble associated with the provided key and if its diff --git a/native/android/OWNERS b/native/android/OWNERS index ac5a89527ef0..d414ed4cd5e2 100644 --- a/native/android/OWNERS +++ b/native/android/OWNERS @@ -1,3 +1,4 @@ per-file libandroid_net.map.txt, net.c = set noparent per-file libandroid_net.map.txt, net.c = codewiz@google.com, jchalard@google.com, junyulai@google.com per-file libandroid_net.map.txt, net.c = lorenzo@google.com, reminv@google.com, satk@google.com +per-file system_fonts.cpp = file:/graphics/java/android/graphics/fonts/OWNERS diff --git a/packages/SystemUI/res/drawable/brightness_progress_drawable_thick.xml b/packages/SystemUI/res/drawable/brightness_progress_drawable_thick.xml index 108591beb05a..d097472471b0 100644 --- a/packages/SystemUI/res/drawable/brightness_progress_drawable_thick.xml +++ b/packages/SystemUI/res/drawable/brightness_progress_drawable_thick.xml @@ -15,22 +15,24 @@ ~ limitations under the License. --> <layer-list xmlns:android="http://schemas.android.com/apk/res/android" - android:paddingMode="stack"> + android:paddingMode="stack" > <item android:id="@android:id/background" android:gravity="center_vertical|fill_horizontal"> - <layer-list > + <layer-list> <item> <shape android:tint="?android:attr/colorControlActivated" android:alpha="?android:attr/disabledAlpha"> - <size android:height="48dp" /> + <size android:height="@dimen/rounded_slider_height" /> <solid android:color="@color/white_disabled" /> - <corners android:radius="24dp" /> + <corners android:radius="@dimen/rounded_slider_corner_radius" /> </shape> </item> <item - android:gravity="center_vertical|start" - android:start="32dp"> + android:gravity="center_vertical|left" + android:height="@dimen/rounded_slider_icon_size" + android:width="@dimen/rounded_slider_icon_size" + android:left="@dimen/rounded_slider_icon_inset"> <com.android.systemui.util.AlphaTintDrawableWrapper android:drawable="@drawable/ic_brightness" android:tint="?android:attr/colorControlActivated" /> @@ -39,10 +41,8 @@ </item> <item android:id="@android:id/progress" android:gravity="center_vertical|fill_horizontal"> - <clip - android:drawable="@drawable/brightness_progress_full_drawable" - android:clipOrientation="horizontal" - android:gravity="left" - /> + <com.android.systemui.util.RoundedCornerProgressDrawable + android:drawable="@drawable/brightness_progress_full_drawable" + /> </item> </layer-list>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/brightness_progress_full_drawable.xml b/packages/SystemUI/res/drawable/brightness_progress_full_drawable.xml index b5def5ebf539..41140a7a8c85 100644 --- a/packages/SystemUI/res/drawable/brightness_progress_full_drawable.xml +++ b/packages/SystemUI/res/drawable/brightness_progress_full_drawable.xml @@ -15,18 +15,21 @@ ~ limitations under the License. --> -<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android" + android:autoMirrored="true"> <item android:id="@+id/slider_foreground"> <shape> - <size android:height="48dp" /> + <size android:height="@dimen/rounded_slider_height" /> <solid android:color="?android:attr/colorControlActivated" /> - <corners android:radius="24dp"/> + <corners android:radius="@dimen/rounded_slider_corner_radius"/> </shape> </item> <item android:id="@+id/slider_icon" - android:gravity="center_vertical|start" - android:start="32dp"> + android:gravity="center_vertical|right" + android:height="@dimen/rounded_slider_icon_size" + android:width="@dimen/rounded_slider_icon_size" + android:right="@dimen/rounded_slider_icon_inset"> <com.android.systemui.util.AlphaTintDrawableWrapper android:drawable="@drawable/ic_brightness" android:tint="?android:attr/colorBackground" diff --git a/packages/SystemUI/res/values/attrs.xml b/packages/SystemUI/res/values/attrs.xml index e2fe223f591b..6c55fb62e638 100644 --- a/packages/SystemUI/res/values/attrs.xml +++ b/packages/SystemUI/res/values/attrs.xml @@ -177,5 +177,9 @@ <attr name="handleColor" format="color" /> <attr name="scrimColor" format="color" /> </declare-styleable> + + <declare-styleable name="RoundedCornerProgressDrawable"> + <attr name="android:drawable" /> + </declare-styleable> </resources> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 1fac96bb181d..d92f4ea65390 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1333,4 +1333,12 @@ <dimen name="people_space_widget_radius">24dp</dimen> <dimen name="people_space_widget_round_radius">100dp</dimen> <dimen name="people_space_widget_background_padding">6dp</dimen> + + <dimen name="rounded_slider_height">48dp</dimen> + <!-- rounded_slider_height / 2 --> + <dimen name="rounded_slider_corner_radius">24dp</dimen> + <!-- rounded_slider_height / 2 --> + <dimen name="rounded_slider_icon_size">24dp</dimen> + <!-- rounded_slider_icon_size / 2 --> + <dimen name="rounded_slider_icon_inset">12dp</dimen> </resources> diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java index 3bc5ebf8c64e..5c650ad3982e 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java @@ -325,8 +325,6 @@ public class ScreenshotView extends FrameLayout implements Log.d(TAG, "createAnim: bounds=" + bounds + " showFlash=" + showFlash); } - Rect previewBounds = new Rect(); - mScreenshotPreview.getBoundsOnScreen(previewBounds); Rect targetPosition = new Rect(); mScreenshotPreview.getHitRect(targetPosition); diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessControllerSettings.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessControllerSettings.java index 8dcc8b46f024..3c7d78c928fa 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessControllerSettings.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessControllerSettings.java @@ -18,7 +18,6 @@ package com.android.systemui.settings.brightness; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.statusbar.FeatureFlags; -import com.android.systemui.util.settings.SecureSettings; import javax.inject.Inject; @@ -30,25 +29,21 @@ public class BrightnessControllerSettings { private static final String THICK_BRIGHTNESS_SLIDER = "sysui_thick_brightness"; private final FeatureFlags mFeatureFlags; - private final boolean mUseThickSlider; - private final boolean mUseMirrorOnThickSlider; @Inject - public BrightnessControllerSettings(SecureSettings settings, FeatureFlags featureFlags) { + public BrightnessControllerSettings(FeatureFlags featureFlags) { mFeatureFlags = featureFlags; - mUseThickSlider = settings.getInt(THICK_BRIGHTNESS_SLIDER, 0) != 0; - mUseMirrorOnThickSlider = settings.getInt(THICK_BRIGHTNESS_SLIDER, 0) != 2; } // Changing this setting between zero and non-zero may crash systemui down the line. Better to // restart systemui after changing it. /** */ boolean useThickSlider() { - return mUseThickSlider && mFeatureFlags.useNewBrightnessSlider(); + return mFeatureFlags.useNewBrightnessSlider(); } /** */ boolean useMirrorOnThickSlider() { - return !useThickSlider() || (useThickSlider() && mUseMirrorOnThickSlider); + return !useThickSlider(); } } diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSlider.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSlider.java index 53ff1dfd277b..a6aec3b7b1b7 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSlider.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSlider.java @@ -17,7 +17,6 @@ package com.android.systemui.settings.brightness; import android.content.Context; -import android.graphics.drawable.ClipDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.view.LayoutInflater; @@ -33,6 +32,7 @@ import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.Utils; import com.android.systemui.R; import com.android.systemui.statusbar.policy.BrightnessMirrorController; +import com.android.systemui.util.RoundedCornerProgressDrawable; import com.android.systemui.util.ViewController; import javax.inject.Inject; @@ -292,8 +292,8 @@ public class BrightnessSlider if (b.getProgressDrawable() instanceof LayerDrawable) { Drawable progress = ((LayerDrawable) b.getProgressDrawable()) .findDrawableByLayerId(com.android.internal.R.id.progress); - if (progress instanceof ClipDrawable) { - Drawable inner = ((ClipDrawable) progress).getDrawable(); + if (progress instanceof RoundedCornerProgressDrawable) { + Drawable inner = ((RoundedCornerProgressDrawable) progress).getDrawable(); if (inner instanceof LayerDrawable) { return (LayerDrawable) inner; } diff --git a/packages/SystemUI/src/com/android/systemui/util/RoundedCornerProgressDrawable.kt b/packages/SystemUI/src/com/android/systemui/util/RoundedCornerProgressDrawable.kt new file mode 100644 index 000000000000..1af2c9f46373 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/RoundedCornerProgressDrawable.kt @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util + +import android.content.res.Resources +import android.content.res.TypedArray +import android.graphics.Canvas +import android.graphics.Path +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.graphics.drawable.DrawableWrapper +import android.util.AttributeSet +import com.android.systemui.R +import org.xmlpull.v1.XmlPullParser + +/** + * [DrawableWrapper] to use in the progress of a slider. + * + * This drawable is used to change the bounds of the enclosed drawable depending on the level to + * simulate a sliding progress, instead of using clipping or scaling. That way, the shape of the + * edges is maintained. + * + * Meant to be used with a rounded ends background, it will also prevent deformation when the slider + * is meant to be smaller than the rounded corner. The background should have rounded corners that + * are half of the height. + */ +class RoundedCornerProgressDrawable(drawable: Drawable?) : DrawableWrapper(drawable) { + + constructor() : this(null) + + companion object { + private const val MAX_LEVEL = 10000 // Taken from Drawable + } + + private var clipPath: Path = Path() + + init { + setClipPath(Rect()) + } + + override fun inflate( + r: Resources, + parser: XmlPullParser, + attrs: AttributeSet, + theme: Resources.Theme? + ) { + val a = obtainAttributes(r, theme, attrs, R.styleable.RoundedCornerProgressDrawable) + + // Inflation will advance the XmlPullParser and AttributeSet. + super.inflate(r, parser, attrs, theme) + + updateStateFromTypedArray(a) + if (drawable == null) { + throw IllegalStateException("${this::class.java.simpleName} needs a drawable") + } + a.recycle() + } + + override fun onLayoutDirectionChanged(layoutDirection: Int): Boolean { + onLevelChange(level) + return super.onLayoutDirectionChanged(layoutDirection) + } + + private fun updateStateFromTypedArray(a: TypedArray) { + if (a.hasValue(R.styleable.RoundedCornerProgressDrawable_android_drawable)) { + setDrawable(a.getDrawable(R.styleable.RoundedCornerProgressDrawable_android_drawable)) + } + } + + override fun onBoundsChange(bounds: Rect) { + setClipPath(bounds) + super.onBoundsChange(bounds) + onLevelChange(level) + } + + private fun setClipPath(bounds: Rect) { + clipPath.reset() + clipPath.addRoundRect( + bounds.left.toFloat(), + bounds.top.toFloat(), + bounds.right.toFloat(), + bounds.bottom.toFloat(), + bounds.height().toFloat() / 2, + bounds.height().toFloat() / 2, + Path.Direction.CW + ) + } + + override fun onLevelChange(level: Int): Boolean { + val db = drawable?.bounds!! + val width = bounds.width() * level / MAX_LEVEL + // Extra space on the left to keep the rounded shape on the right end + val leftBound = bounds.left - bounds.height() + drawable?.setBounds(leftBound, db.top, bounds.left + width, db.bottom) + return super.onLevelChange(level) + } + + override fun draw(canvas: Canvas) { + canvas.save() + canvas.clipPath(clipPath) + super.draw(canvas) + canvas.restore() + } +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/apphibernation/AppHibernationService.java b/services/core/java/com/android/server/apphibernation/AppHibernationService.java index fc48e2df39e8..e97f0b47380a 100644 --- a/services/core/java/com/android/server/apphibernation/AppHibernationService.java +++ b/services/core/java/com/android/server/apphibernation/AppHibernationService.java @@ -20,7 +20,7 @@ import static android.content.Intent.ACTION_PACKAGE_ADDED; import static android.content.Intent.ACTION_PACKAGE_REMOVED; import static android.content.Intent.EXTRA_REMOVED_FOR_ALL_USERS; import static android.content.Intent.EXTRA_REPLACING; -import static android.content.pm.PackageManager.MATCH_ALL; +import static android.content.pm.PackageManager.MATCH_ANY_USER; import static android.provider.DeviceConfig.NAMESPACE_APP_HIBERNATION; import android.annotation.NonNull; @@ -34,7 +34,9 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.pm.IPackageManager; import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import android.os.Binder; +import android.os.Environment; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ServiceManager; @@ -52,10 +54,14 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.server.SystemService; +import java.io.File; import java.io.FileDescriptor; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; /** * System service that manages app hibernation state, a state apps can enter that means they are @@ -64,6 +70,11 @@ import java.util.Set; */ public final class AppHibernationService extends SystemService { private static final String TAG = "AppHibernationService"; + private static final int PACKAGE_MATCH_FLAGS = + PackageManager.MATCH_DIRECT_BOOT_AWARE + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE + | PackageManager.MATCH_UNINSTALLED_PACKAGES + | PackageManager.MATCH_DISABLED_COMPONENTS; /** * Lock for accessing any in-memory hibernation state @@ -74,9 +85,13 @@ public final class AppHibernationService extends SystemService { private final IActivityManager mIActivityManager; private final UserManager mUserManager; @GuardedBy("mLock") - private final SparseArray<Map<String, UserPackageState>> mUserStates = new SparseArray<>(); + private final SparseArray<Map<String, UserLevelState>> mUserStates = new SparseArray<>(); + private final SparseArray<HibernationStateDiskStore<UserLevelState>> mUserDiskStores = + new SparseArray<>(); @GuardedBy("mLock") - private final Set<String> mGloballyHibernatedPackages = new ArraySet<>(); + private final Map<String, GlobalLevelState> mGlobalHibernationStates = new ArrayMap<>(); + private final HibernationStateDiskStore<GlobalLevelState> mGlobalLevelHibernationDiskStore; + private final Injector mInjector; /** * Initializes the system service. @@ -88,19 +103,18 @@ public final class AppHibernationService extends SystemService { * @param context The system server context. */ public AppHibernationService(@NonNull Context context) { - this(context, IPackageManager.Stub.asInterface(ServiceManager.getService("package")), - ActivityManager.getService(), - context.getSystemService(UserManager.class)); + this(new InjectorImpl(context)); } @VisibleForTesting - AppHibernationService(@NonNull Context context, IPackageManager packageManager, - IActivityManager activityManager, UserManager userManager) { - super(context); - mContext = context; - mIPackageManager = packageManager; - mIActivityManager = activityManager; - mUserManager = userManager; + AppHibernationService(@NonNull Injector injector) { + super(injector.getContext()); + mContext = injector.getContext(); + mIPackageManager = injector.getPackageManager(); + mIActivityManager = injector.getActivityManager(); + mUserManager = injector.getUserManager(); + mGlobalLevelHibernationDiskStore = injector.getGlobalLevelDiskStore(); + mInjector = injector; final Context userAllContext = mContext.createContextAsUser(UserHandle.ALL, 0 /* flags */); @@ -116,6 +130,17 @@ public final class AppHibernationService extends SystemService { publishBinderService(Context.APP_HIBERNATION_SERVICE, mServiceStub); } + @Override + public void onBootPhase(int phase) { + if (phase == PHASE_BOOT_COMPLETED) { + List<GlobalLevelState> states = + mGlobalLevelHibernationDiskStore.readHibernationStates(); + synchronized (mLock) { + initializeGlobalHibernationStates(states); + } + } + } + /** * Whether a package is hibernating for a given user. * @@ -131,8 +156,8 @@ public final class AppHibernationService extends SystemService { return false; } synchronized (mLock) { - final Map<String, UserPackageState> packageStates = mUserStates.get(userId); - final UserPackageState pkgState = packageStates.get(packageName); + final Map<String, UserLevelState> packageStates = mUserStates.get(userId); + final UserLevelState pkgState = packageStates.get(packageName); if (pkgState == null) { throw new IllegalArgumentException( String.format("Package %s is not installed for user %s", @@ -150,7 +175,12 @@ public final class AppHibernationService extends SystemService { */ boolean isHibernatingGlobally(String packageName) { synchronized (mLock) { - return mGloballyHibernatedPackages.contains(packageName); + GlobalLevelState state = mGlobalHibernationStates.get(packageName); + if (state == null) { + throw new IllegalArgumentException( + String.format("Package %s is not installed", packageName)); + } + return state.hibernated; } } @@ -169,8 +199,8 @@ public final class AppHibernationService extends SystemService { return; } synchronized (mLock) { - Map<String, UserPackageState> packageStates = mUserStates.get(userId); - UserPackageState pkgState = packageStates.get(packageName); + final Map<String, UserLevelState> packageStates = mUserStates.get(userId); + final UserLevelState pkgState = packageStates.get(packageName); if (pkgState == null) { throw new IllegalArgumentException( String.format("Package %s is not installed for user %s", @@ -182,10 +212,12 @@ public final class AppHibernationService extends SystemService { } if (isHibernating) { - hibernatePackageForUserL(packageName, userId, pkgState); + hibernatePackageForUser(packageName, userId, pkgState); } else { - unhibernatePackageForUserL(packageName, userId, pkgState); + unhibernatePackageForUser(packageName, userId, pkgState); } + List<UserLevelState> states = new ArrayList<>(mUserStates.get(userId).values()); + mUserDiskStores.get(userId).scheduleWriteHibernationStates(states); } } @@ -197,25 +229,32 @@ public final class AppHibernationService extends SystemService { * @param isHibernating new hibernation state */ void setHibernatingGlobally(String packageName, boolean isHibernating) { - if (isHibernating != mGloballyHibernatedPackages.contains(packageName)) { - synchronized (mLock) { + synchronized (mLock) { + GlobalLevelState state = mGlobalHibernationStates.get(packageName); + if (state == null) { + throw new IllegalArgumentException( + String.format("Package %s is not installed for any user", packageName)); + } + if (state.hibernated != isHibernating) { if (isHibernating) { - hibernatePackageGloballyL(packageName); + hibernatePackageGlobally(packageName, state); } else { - unhibernatePackageGloballyL(packageName); + unhibernatePackageGlobally(packageName, state); } + List<GlobalLevelState> states = new ArrayList<>(mGlobalHibernationStates.values()); + mGlobalLevelHibernationDiskStore.scheduleWriteHibernationStates(states); } } } /** * Put an app into hibernation for a given user, allowing user-level optimizations to occur. - * The caller should hold {@link #mLock} * * @param pkgState package hibernation state */ - private void hibernatePackageForUserL(@NonNull String packageName, int userId, - @NonNull UserPackageState pkgState) { + @GuardedBy("mLock") + private void hibernatePackageForUser(@NonNull String packageName, int userId, + @NonNull UserLevelState pkgState) { Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "hibernatePackage"); final long caller = Binder.clearCallingIdentity(); try { @@ -233,12 +272,13 @@ public final class AppHibernationService extends SystemService { } /** - * Remove a package from hibernation for a given user. The caller should hold {@link #mLock}. + * Remove a package from hibernation for a given user. * * @param pkgState package hibernation state */ - private void unhibernatePackageForUserL(@NonNull String packageName, int userId, - UserPackageState pkgState) { + @GuardedBy("mLock") + private void unhibernatePackageForUser(@NonNull String packageName, int userId, + UserLevelState pkgState) { Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "unhibernatePackage"); final long caller = Binder.clearCallingIdentity(); try { @@ -255,64 +295,140 @@ public final class AppHibernationService extends SystemService { /** * Put a package into global hibernation, optimizing its storage at a package / APK level. - * The caller should hold {@link #mLock}. */ - private void hibernatePackageGloballyL(@NonNull String packageName) { + @GuardedBy("mLock") + private void hibernatePackageGlobally(@NonNull String packageName, GlobalLevelState state) { Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "hibernatePackageGlobally"); // TODO(175830194): Delete vdex/odex when DexManager API is built out - mGloballyHibernatedPackages.add(packageName); + state.hibernated = true; Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); } /** - * Unhibernate a package from global hibernation. The caller should hold {@link #mLock}. + * Unhibernate a package from global hibernation. */ - private void unhibernatePackageGloballyL(@NonNull String packageName) { + @GuardedBy("mLock") + private void unhibernatePackageGlobally(@NonNull String packageName, GlobalLevelState state) { Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "unhibernatePackageGlobally"); - mGloballyHibernatedPackages.remove(packageName); + state.hibernated = false; Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); } /** - * Populates {@link #mUserStates} with the users installed packages. The caller should hold - * {@link #mLock}. + * Initializes in-memory store of user-level hibernation states for the given user * * @param userId user id to add installed packages for + * @param diskStates states pulled from disk, if available + */ + @GuardedBy("mLock") + private void initializeUserHibernationStates(int userId, + @Nullable List<UserLevelState> diskStates) { + List<PackageInfo> packages; + try { + packages = mIPackageManager.getInstalledPackages(PACKAGE_MATCH_FLAGS, userId).getList(); + } catch (RemoteException e) { + throw new IllegalStateException("Package manager not available", e); + } + + Map<String, UserLevelState> userLevelStates = new ArrayMap<>(); + + for (int i = 0, size = packages.size(); i < size; i++) { + String packageName = packages.get(i).packageName; + UserLevelState state = new UserLevelState(); + state.packageName = packageName; + userLevelStates.put(packageName, state); + } + + if (diskStates != null) { + Set<String> installedPackages = new ArraySet<>(); + for (int i = 0, size = packages.size(); i < size; i++) { + installedPackages.add(packages.get(i).packageName); + } + for (int i = 0, size = diskStates.size(); i < size; i++) { + String packageName = diskStates.get(i).packageName; + if (!installedPackages.contains(packageName)) { + Slog.w(TAG, String.format( + "No hibernation state associated with package %s user %d. Maybe" + + "the package was uninstalled? ", packageName, userId)); + continue; + } + userLevelStates.put(packageName, diskStates.get(i)); + } + } + mUserStates.put(userId, userLevelStates); + } + + /** + * Initialize in-memory store of global level hibernation states. + * + * @param diskStates global level hibernation states pulled from disk, if available */ - private void addUserPackageStatesL(int userId) { - Map<String, UserPackageState> packages = new ArrayMap<>(); - List<PackageInfo> packageList; + @GuardedBy("mLock") + private void initializeGlobalHibernationStates(@Nullable List<GlobalLevelState> diskStates) { + List<PackageInfo> packages; try { - packageList = mIPackageManager.getInstalledPackages(MATCH_ALL, userId).getList(); + packages = mIPackageManager.getInstalledPackages( + PACKAGE_MATCH_FLAGS | MATCH_ANY_USER, 0 /* userId */).getList(); } catch (RemoteException e) { - throw new IllegalStateException("Package manager not available.", e); + throw new IllegalStateException("Package manager not available", e); } - for (int i = 0, size = packageList.size(); i < size; i++) { - packages.put(packageList.get(i).packageName, new UserPackageState()); + for (int i = 0, size = packages.size(); i < size; i++) { + String packageName = packages.get(i).packageName; + GlobalLevelState state = new GlobalLevelState(); + state.packageName = packageName; + mGlobalHibernationStates.put(packageName, state); + } + if (diskStates != null) { + Set<String> installedPackages = new ArraySet<>(); + for (int i = 0, size = packages.size(); i < size; i++) { + installedPackages.add(packages.get(i).packageName); + } + for (int i = 0, size = diskStates.size(); i < size; i++) { + GlobalLevelState state = diskStates.get(i); + if (!installedPackages.contains(state.packageName)) { + Slog.w(TAG, String.format( + "No hibernation state associated with package %s. Maybe the " + + "package was uninstalled? ", state.packageName)); + continue; + } + mGlobalHibernationStates.put(state.packageName, state); + } } - mUserStates.put(userId, packages); } @Override public void onUserUnlocking(@NonNull TargetUser user) { - // TODO: Pull from persistent disk storage. For now, just make from scratch. + int userId = user.getUserIdentifier(); + HibernationStateDiskStore<UserLevelState> diskStore = + mInjector.getUserLevelDiskStore(userId); + mUserDiskStores.put(userId, diskStore); + List<UserLevelState> storedStates = diskStore.readHibernationStates(); synchronized (mLock) { - addUserPackageStatesL(user.getUserIdentifier()); + initializeUserHibernationStates(userId, storedStates); } } @Override public void onUserStopping(@NonNull TargetUser user) { + int userId = user.getUserIdentifier(); + // TODO: Flush any scheduled writes to disk immediately on user stopping / power off. synchronized (mLock) { - // TODO: Flush to disk when persistence is implemented - mUserStates.remove(user.getUserIdentifier()); + mUserDiskStores.remove(userId); + mUserStates.remove(userId); } } private void onPackageAdded(@NonNull String packageName, int userId) { synchronized (mLock) { - mUserStates.get(userId).put(packageName, new UserPackageState()); + UserLevelState userState = new UserLevelState(); + userState.packageName = packageName; + mUserStates.get(userId).put(packageName, userState); + if (!mGlobalHibernationStates.containsKey(packageName)) { + GlobalLevelState globalState = new GlobalLevelState(); + globalState.packageName = packageName; + mGlobalHibernationStates.put(packageName, globalState); + } } } @@ -324,7 +440,7 @@ public final class AppHibernationService extends SystemService { private void onPackageRemovedForAllUsers(@NonNull String packageName) { synchronized (mLock) { - mGloballyHibernatedPackages.remove(packageName); + mGlobalHibernationStates.remove(packageName); } } @@ -425,10 +541,66 @@ public final class AppHibernationService extends SystemService { } /** - * Data class that contains hibernation state info of a package for a user. + * Dependency injector for {@link #AppHibernationService)}. */ - private static final class UserPackageState { - public boolean hibernated; - // TODO: Track whether hibernation is exempted by the user + interface Injector { + Context getContext(); + + IPackageManager getPackageManager(); + + IActivityManager getActivityManager(); + + UserManager getUserManager(); + + HibernationStateDiskStore<GlobalLevelState> getGlobalLevelDiskStore(); + + HibernationStateDiskStore<UserLevelState> getUserLevelDiskStore(int userId); + } + + private static final class InjectorImpl implements Injector { + private static final String HIBERNATION_DIR_NAME = "hibernation"; + private final Context mContext; + private final ScheduledExecutorService mScheduledExecutorService; + private final UserLevelHibernationProto mUserLevelHibernationProto; + + InjectorImpl(Context context) { + mContext = context; + mScheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); + mUserLevelHibernationProto = new UserLevelHibernationProto(); + } + + @Override + public Context getContext() { + return mContext; + } + + @Override + public IPackageManager getPackageManager() { + return IPackageManager.Stub.asInterface(ServiceManager.getService("package")); + } + + @Override + public IActivityManager getActivityManager() { + return ActivityManager.getService(); + } + + @Override + public UserManager getUserManager() { + return mContext.getSystemService(UserManager.class); + } + + @Override + public HibernationStateDiskStore<GlobalLevelState> getGlobalLevelDiskStore() { + File dir = new File(Environment.getDataSystemDirectory(), HIBERNATION_DIR_NAME); + return new HibernationStateDiskStore<>( + dir, new GlobalLevelHibernationProto(), mScheduledExecutorService); + } + + @Override + public HibernationStateDiskStore<UserLevelState> getUserLevelDiskStore(int userId) { + File dir = new File(Environment.getDataSystemCeDirectory(userId), HIBERNATION_DIR_NAME); + return new HibernationStateDiskStore<>( + dir, mUserLevelHibernationProto, mScheduledExecutorService); + } } } diff --git a/services/core/java/com/android/server/apphibernation/GlobalLevelHibernationProto.java b/services/core/java/com/android/server/apphibernation/GlobalLevelHibernationProto.java new file mode 100644 index 000000000000..79e995b038fa --- /dev/null +++ b/services/core/java/com/android/server/apphibernation/GlobalLevelHibernationProto.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2021 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.apphibernation; + + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.Slog; +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoOutputStream; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Reads and writes protos for {@link GlobalLevelState} hiberation states. + */ +final class GlobalLevelHibernationProto implements ProtoReadWriter<List<GlobalLevelState>> { + private static final String TAG = "GlobalLevelHibernationProtoReadWriter"; + + @Override + public void writeToProto(@NonNull ProtoOutputStream stream, + @NonNull List<GlobalLevelState> data) { + for (int i = 0, size = data.size(); i < size; i++) { + long token = stream.start(GlobalLevelHibernationStatesProto.HIBERNATION_STATE); + GlobalLevelState state = data.get(i); + stream.write(GlobalLevelHibernationStateProto.PACKAGE_NAME, state.packageName); + stream.write(GlobalLevelHibernationStateProto.HIBERNATED, state.hibernated); + stream.end(token); + } + } + + @Override + public @Nullable List<GlobalLevelState> readFromProto(@NonNull ProtoInputStream stream) + throws IOException { + List<GlobalLevelState> list = new ArrayList<>(); + while (stream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + if (stream.getFieldNumber() + != (int) GlobalLevelHibernationStatesProto.HIBERNATION_STATE) { + continue; + } + GlobalLevelState state = new GlobalLevelState(); + long token = stream.start(GlobalLevelHibernationStatesProto.HIBERNATION_STATE); + while (stream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (stream.getFieldNumber()) { + case (int) GlobalLevelHibernationStateProto.PACKAGE_NAME: + state.packageName = + stream.readString(GlobalLevelHibernationStateProto.PACKAGE_NAME); + break; + case (int) GlobalLevelHibernationStateProto.HIBERNATED: + state.hibernated = + stream.readBoolean(GlobalLevelHibernationStateProto.HIBERNATED); + break; + default: + Slog.w(TAG, "Undefined field in proto: " + stream.getFieldNumber()); + } + } + stream.end(token); + list.add(state); + } + return list; + } +} diff --git a/services/core/java/com/android/server/apphibernation/GlobalLevelState.java b/services/core/java/com/android/server/apphibernation/GlobalLevelState.java new file mode 100644 index 000000000000..4f756756c2ab --- /dev/null +++ b/services/core/java/com/android/server/apphibernation/GlobalLevelState.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2021 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.apphibernation; + +/** + * Data class that contains global hibernation state for a package. + */ +final class GlobalLevelState { + public String packageName; + public boolean hibernated; +} diff --git a/services/core/java/com/android/server/apphibernation/HibernationStateDiskStore.java b/services/core/java/com/android/server/apphibernation/HibernationStateDiskStore.java new file mode 100644 index 000000000000..c83659d2ff56 --- /dev/null +++ b/services/core/java/com/android/server/apphibernation/HibernationStateDiskStore.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2021 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.apphibernation; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.WorkerThread; +import android.text.format.DateUtils; +import android.util.AtomicFile; +import android.util.Slog; +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.annotations.VisibleForTesting; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +/** + * Disk store utility class for hibernation states. + * + * @param <T> the type of hibernation state data + */ +class HibernationStateDiskStore<T> { + private static final String TAG = "HibernationStateDiskStore"; + + // Time to wait before actually writing. Saves extra writes if data changes come in batches. + private static final long DISK_WRITE_DELAY = 1L * DateUtils.MINUTE_IN_MILLIS; + private static final String STATES_FILE_NAME = "states"; + + private final File mHibernationFile; + private final ScheduledExecutorService mExecutorService; + private final ProtoReadWriter<List<T>> mProtoReadWriter; + private List<T> mScheduledStatesToWrite = new ArrayList<>(); + private ScheduledFuture<?> mFuture; + + /** + * Initialize a disk store for hibernation states in the given directory. + * + * @param hibernationDir directory to write/read states file + * @param readWriter writer/reader of states proto + * @param executorService scheduled executor for writing data + */ + HibernationStateDiskStore(@NonNull File hibernationDir, + @NonNull ProtoReadWriter<List<T>> readWriter, + @NonNull ScheduledExecutorService executorService) { + this(hibernationDir, readWriter, executorService, STATES_FILE_NAME); + } + + @VisibleForTesting + HibernationStateDiskStore(@NonNull File hibernationDir, + @NonNull ProtoReadWriter<List<T>> readWriter, + @NonNull ScheduledExecutorService executorService, + @NonNull String fileName) { + mHibernationFile = new File(hibernationDir, fileName); + mExecutorService = executorService; + mProtoReadWriter = readWriter; + } + + /** + * Schedule a full write of all the hibernation states to the file on disk. Does not run + * immediately and subsequent writes override previous ones. + * + * @param hibernationStates list of hibernation states to write to disk + */ + void scheduleWriteHibernationStates(@NonNull List<T> hibernationStates) { + synchronized (this) { + mScheduledStatesToWrite = hibernationStates; + if (mExecutorService.isShutdown()) { + Slog.e(TAG, "Scheduled executor service is shut down."); + return; + } + + // Already have write scheduled + if (mFuture != null) { + Slog.i(TAG, "Write already scheduled. Skipping schedule."); + return; + } + + mFuture = mExecutorService.schedule(this::writeHibernationStates, DISK_WRITE_DELAY, + TimeUnit.MILLISECONDS); + } + } + + /** + * Read hibernation states from disk. + * + * @return the parsed list of hibernation states, null if file does not exist + */ + @Nullable + List<T> readHibernationStates() { + synchronized (this) { + if (!mHibernationFile.exists()) { + Slog.i(TAG, "No hibernation file on disk for file " + mHibernationFile.getPath()); + return null; + } + AtomicFile atomicFile = new AtomicFile(mHibernationFile); + + try { + FileInputStream inputStream = atomicFile.openRead(); + ProtoInputStream protoInputStream = new ProtoInputStream(inputStream); + return mProtoReadWriter.readFromProto(protoInputStream); + } catch (IOException e) { + Slog.e(TAG, "Failed to read states protobuf.", e); + return null; + } + } + } + + @WorkerThread + private void writeHibernationStates() { + synchronized (this) { + writeStateProto(mScheduledStatesToWrite); + mScheduledStatesToWrite.clear(); + mFuture = null; + } + } + + @WorkerThread + private void writeStateProto(List<T> states) { + AtomicFile atomicFile = new AtomicFile(mHibernationFile); + + FileOutputStream fileOutputStream; + try { + fileOutputStream = atomicFile.startWrite(); + } catch (IOException e) { + Slog.e(TAG, "Failed to start write to states protobuf.", e); + return; + } + + try { + ProtoOutputStream protoOutputStream = new ProtoOutputStream(fileOutputStream); + mProtoReadWriter.writeToProto(protoOutputStream, states); + protoOutputStream.flush(); + atomicFile.finishWrite(fileOutputStream); + } catch (Exception e) { + Slog.e(TAG, "Failed to finish write to states protobuf.", e); + atomicFile.failWrite(fileOutputStream); + } + } +} diff --git a/services/core/java/com/android/server/apphibernation/ProtoReadWriter.java b/services/core/java/com/android/server/apphibernation/ProtoReadWriter.java new file mode 100644 index 000000000000..0cbc09a7a99d --- /dev/null +++ b/services/core/java/com/android/server/apphibernation/ProtoReadWriter.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2021 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.apphibernation; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoOutputStream; + +import java.io.IOException; + +/** + * Proto utility that reads and writes proto for some data. + * + * @param <T> data that can be written and read from a proto + */ +interface ProtoReadWriter<T> { + + /** + * Write data to a proto stream + */ + void writeToProto(@NonNull ProtoOutputStream stream, @NonNull T data); + + /** + * Parse data from the proto stream and return + */ + @Nullable T readFromProto(@NonNull ProtoInputStream stream) throws IOException; +} diff --git a/services/core/java/com/android/server/apphibernation/UserLevelHibernationProto.java b/services/core/java/com/android/server/apphibernation/UserLevelHibernationProto.java new file mode 100644 index 000000000000..a24c4c575975 --- /dev/null +++ b/services/core/java/com/android/server/apphibernation/UserLevelHibernationProto.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2021 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.apphibernation; + + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.Slog; +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoOutputStream; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Reads and writes protos for {@link UserLevelState} hiberation states. + */ +final class UserLevelHibernationProto implements ProtoReadWriter<List<UserLevelState>> { + private static final String TAG = "UserLevelHibernationProtoReadWriter"; + + @Override + public void writeToProto(@NonNull ProtoOutputStream stream, + @NonNull List<UserLevelState> data) { + for (int i = 0, size = data.size(); i < size; i++) { + long token = stream.start(UserLevelHibernationStatesProto.HIBERNATION_STATE); + UserLevelState state = data.get(i); + stream.write(UserLevelHibernationStateProto.PACKAGE_NAME, state.packageName); + stream.write(UserLevelHibernationStateProto.HIBERNATED, state.hibernated); + stream.end(token); + } + } + + @Override + public @Nullable List<UserLevelState> readFromProto(@NonNull ProtoInputStream stream) + throws IOException { + List<UserLevelState> list = new ArrayList<>(); + while (stream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + if (stream.getFieldNumber() + != (int) UserLevelHibernationStatesProto.HIBERNATION_STATE) { + continue; + } + UserLevelState state = new UserLevelState(); + long token = stream.start(UserLevelHibernationStatesProto.HIBERNATION_STATE); + while (stream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (stream.getFieldNumber()) { + case (int) UserLevelHibernationStateProto.PACKAGE_NAME: + state.packageName = + stream.readString(UserLevelHibernationStateProto.PACKAGE_NAME); + break; + case (int) UserLevelHibernationStateProto.HIBERNATED: + state.hibernated = + stream.readBoolean(UserLevelHibernationStateProto.HIBERNATED); + break; + default: + Slog.w(TAG, "Undefined field in proto: " + stream.getFieldNumber()); + } + } + stream.end(token); + list.add(state); + } + return list; + } +} diff --git a/services/core/java/com/android/server/apphibernation/UserLevelState.java b/services/core/java/com/android/server/apphibernation/UserLevelState.java new file mode 100644 index 000000000000..c66dad87c891 --- /dev/null +++ b/services/core/java/com/android/server/apphibernation/UserLevelState.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2021 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.apphibernation; + +/** + * Data class that contains hibernation state info of a package for a user. + */ +final class UserLevelState { + public String packageName; + public boolean hibernated; +} diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index f07da8f0236b..10fe1e1d0684 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -2796,6 +2796,8 @@ public class AppOpsService extends IAppOpsService.Stub { if (callback == null) { return; } + final boolean mayWatchPackageName = + packageName != null && !filterAppAccessUnlocked(packageName); synchronized (this) { int switchOp = (op != AppOpsManager.OP_NONE) ? AppOpsManager.opToSwitch(op) : op; @@ -2824,7 +2826,7 @@ public class AppOpsService extends IAppOpsService.Stub { } cbs.add(cb); } - if (packageName != null) { + if (mayWatchPackageName) { ArraySet<ModeCallback> cbs = mPackageModeWatchers.get(packageName); if (cbs == null) { cbs = new ArraySet<>(); @@ -3008,13 +3010,27 @@ public class AppOpsService extends IAppOpsService.Stub { Objects.requireNonNull(packageName); try { verifyAndGetBypass(uid, packageName, null); - + if (filterAppAccessUnlocked(packageName)) { + return AppOpsManager.MODE_ERRORED; + } return AppOpsManager.MODE_ALLOWED; } catch (SecurityException ignored) { return AppOpsManager.MODE_ERRORED; } } + /** + * This method will check with PackageManager to determine if the package provided should + * be visible to the {@link Binder#getCallingUid()}. + * + * NOTE: This must not be called while synchronized on {@code this} to avoid dead locks + */ + private boolean filterAppAccessUnlocked(String packageName) { + final int callingUid = Binder.getCallingUid(); + return LocalServices.getService(PackageManagerInternal.class) + .filterAppAccess(packageName, callingUid, UserHandle.getUserId(callingUid)); + } + @Override public int noteProxyOperation(int code, int proxiedUid, String proxiedPackageName, String proxiedAttributionTag, int proxyUid, String proxyPackageName, diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java index 9869f779cc20..1135126d86fb 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java @@ -113,6 +113,7 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider @NonNull private final HalResultController mHalResultController; @Nullable private IUdfpsOverlayController mUdfpsOverlayController; private int mCurrentUserId = UserHandle.USER_NULL; + private boolean mIsUdfps = false; private final int mSensorId; private final class BiometricTaskStackListener extends TaskStackListener { @@ -335,22 +336,22 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider } final IBiometricsFingerprint daemon = getDaemon(); - boolean isUdfps = false; + mIsUdfps = false; android.hardware.biometrics.fingerprint.V2_3.IBiometricsFingerprint extension = android.hardware.biometrics.fingerprint.V2_3.IBiometricsFingerprint.castFrom( daemon); if (extension != null) { try { - isUdfps = extension.isUdfps(sensorId); + mIsUdfps = extension.isUdfps(sensorId); } catch (RemoteException e) { Slog.e(TAG, "Remote exception while quering udfps", e); - isUdfps = false; + mIsUdfps = false; } } final @FingerprintSensorProperties.SensorType int sensorType = - isUdfps ? FingerprintSensorProperties.TYPE_UDFPS_OPTICAL - : FingerprintSensorProperties.TYPE_REAR; + mIsUdfps ? FingerprintSensorProperties.TYPE_UDFPS_OPTICAL + : FingerprintSensorProperties.TYPE_REAR; // resetLockout is controlled by the framework, so hardwareAuthToken is not required final boolean resetLockoutRequiresHardwareAuthToken = false; final int maxEnrollmentsPerUser = mContext.getResources() @@ -414,7 +415,7 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider Slog.d(TAG, "Daemon was null, reconnecting, current operation: " + mScheduler.getCurrentClient()); try { - mDaemon = IBiometricsFingerprint.getService(); + mDaemon = IBiometricsFingerprint.getService(true /* retry */); } catch (java.util.NoSuchElementException e) { // Service doesn't exist or cannot be opened. Slog.w(TAG, "NoSuchElementException", e); @@ -799,6 +800,7 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider JSONObject dump = new JSONObject(); try { dump.put("service", TAG); + dump.put("isUdfps", mIsUdfps); JSONArray sets = new JSONArray(); for (UserInfo user : UserManager.get(mContext).getUsers()) { diff --git a/services/core/java/com/android/server/graphics/fonts/FontManagerService.java b/services/core/java/com/android/server/graphics/fonts/FontManagerService.java index 90368129e78e..2eabf4054d84 100644 --- a/services/core/java/com/android/server/graphics/fonts/FontManagerService.java +++ b/services/core/java/com/android/server/graphics/fonts/FontManagerService.java @@ -219,7 +219,13 @@ public final class FontManagerService extends IFontManager.Stub { private void initialize() { synchronized (mUpdatableFontDirLock) { if (mUpdatableFontDir == null) { - updateSerializedFontMap(); + synchronized (mSerializedFontMapLock) { + try { + mSerializedFontMap = Typeface.serializeFontMap(Typeface.getSystemFontMap()); + } catch (IOException | ErrnoException e) { + mSerializedFontMap = null; + } + } return; } if (mFontCrashDetector.hasCrashed()) { diff --git a/services/core/java/com/android/server/powerstats/PowerStatsHALWrapper.java b/services/core/java/com/android/server/powerstats/PowerStatsHALWrapper.java index eb9df756dd0e..9a91848475fe 100644 --- a/services/core/java/com/android/server/powerstats/PowerStatsHALWrapper.java +++ b/services/core/java/com/android/server/powerstats/PowerStatsHALWrapper.java @@ -120,7 +120,7 @@ public final class PowerStatsHALWrapper { * @return List of EnergyMeasurement objects containing energy measurements for all * available energy meters. */ - android.hardware.power.stats.EnergyMeasurement[] readEnergyMeters(int[] channelIds); + android.hardware.power.stats.EnergyMeasurement[] readEnergyMeter(int[] channelIds); /** * Returns boolean indicating if connection to power stats HAL was established. @@ -235,13 +235,13 @@ public final class PowerStatsHALWrapper { } @Override - public android.hardware.power.stats.EnergyMeasurement[] readEnergyMeters(int[] channelIds) { + public android.hardware.power.stats.EnergyMeasurement[] readEnergyMeter(int[] channelIds) { android.hardware.power.stats.EnergyMeasurement[] energyMeasurementHAL = null; if (sVintfPowerStats != null) { try { energyMeasurementHAL = - sVintfPowerStats.get().readEnergyMeters(channelIds); + sVintfPowerStats.get().readEnergyMeter(channelIds); } catch (RemoteException e) { if (DEBUG) Slog.d(TAG, "Failed to get energy measurements from PowerStats HAL"); } @@ -311,7 +311,7 @@ public final class PowerStatsHALWrapper { } @Override - public android.hardware.power.stats.EnergyMeasurement[] readEnergyMeters(int[] channelIds) { + public android.hardware.power.stats.EnergyMeasurement[] readEnergyMeter(int[] channelIds) { return nativeReadEnergyMeters(channelIds); } diff --git a/services/core/java/com/android/server/powerstats/PowerStatsLogger.java b/services/core/java/com/android/server/powerstats/PowerStatsLogger.java index 78a227ee170e..e117b0c835bf 100644 --- a/services/core/java/com/android/server/powerstats/PowerStatsLogger.java +++ b/services/core/java/com/android/server/powerstats/PowerStatsLogger.java @@ -69,7 +69,7 @@ public final class PowerStatsLogger extends Handler { // Log power meter data. EnergyMeasurement[] energyMeasurements = - mPowerStatsHALWrapper.readEnergyMeters(new int[0]); + mPowerStatsHALWrapper.readEnergyMeter(new int[0]); mPowerStatsMeterStorage.write( EnergyMeasurementUtils.getProtoBytes(energyMeasurements)); if (DEBUG) EnergyMeasurementUtils.print(energyMeasurements); diff --git a/services/core/java/com/android/server/powerstats/PowerStatsService.java b/services/core/java/com/android/server/powerstats/PowerStatsService.java index be5c2e133486..ea41980c02b1 100644 --- a/services/core/java/com/android/server/powerstats/PowerStatsService.java +++ b/services/core/java/com/android/server/powerstats/PowerStatsService.java @@ -291,6 +291,6 @@ public class PowerStatsService extends SystemService { private void readEnergyMeterAsync(CompletableFuture<EnergyMeasurement[]> future, int[] channelIds) { - future.complete(getPowerStatsHal().readEnergyMeters(channelIds)); + future.complete(getPowerStatsHal().readEnergyMeter(channelIds)); } } diff --git a/services/core/java/com/android/server/powerstats/StatsPullAtomCallbackImpl.java b/services/core/java/com/android/server/powerstats/StatsPullAtomCallbackImpl.java index f8b9601bfd62..7c6999acc666 100644 --- a/services/core/java/com/android/server/powerstats/StatsPullAtomCallbackImpl.java +++ b/services/core/java/com/android/server/powerstats/StatsPullAtomCallbackImpl.java @@ -70,7 +70,7 @@ public class StatsPullAtomCallbackImpl implements StatsManager.StatsPullAtomCall } private int pullOnDevicePowerMeasurement(int atomTag, List<StatsEvent> events) { - EnergyMeasurement[] energyMeasurements = mPowerStatsHALWrapper.readEnergyMeters(new int[0]); + EnergyMeasurement[] energyMeasurements = mPowerStatsHALWrapper.readEnergyMeter(new int[0]); if (energyMeasurements == null) { return StatsManager.PULL_SKIP; } diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index bbf6c7616d46..1ece9870f0c9 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -846,6 +846,8 @@ class RootWindowContainer extends WindowContainer<DisplayContent> Slog.i(TAG, ">>> OPEN TRANSACTION performLayoutAndPlaceSurfaces"); } + // Send any pending task-info changes that were queued-up during a layout deferment + mWmService.mAtmService.mTaskOrganizerController.dispatchPendingEvents(); Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "applySurfaceChanges"); mWmService.openSurfaceTransaction(); try { @@ -862,8 +864,6 @@ class RootWindowContainer extends WindowContainer<DisplayContent> } } - // Send any pending task-info changes that were queued-up during a layout deferment - mWmService.mAtmService.mTaskOrganizerController.dispatchPendingEvents(); mWmService.mAnimator.executeAfterPrepareSurfacesRunnables(); checkAppTransitionReady(surfacePlacer); diff --git a/services/tests/servicestests/src/com/android/server/apphibernation/AppHibernationServiceTest.java b/services/tests/servicestests/src/com/android/server/apphibernation/AppHibernationServiceTest.java index 6777e1a5bc35..1328b91d03f9 100644 --- a/services/tests/servicestests/src/com/android/server/apphibernation/AppHibernationServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/apphibernation/AppHibernationServiceTest.java @@ -16,12 +16,15 @@ package com.android.server.apphibernation; +import static android.content.pm.PackageManager.MATCH_ANY_USER; + import static org.junit.Assert.assertTrue; import static org.mockito.AdditionalAnswers.returnsArgAt; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.intThat; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.verify; @@ -47,6 +50,7 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.util.ArrayList; @@ -75,16 +79,19 @@ public final class AppHibernationServiceTest { private IActivityManager mIActivityManager; @Mock private UserManager mUserManager; + @Mock + private HibernationStateDiskStore<UserLevelState> mHibernationStateDiskStore; @Captor private ArgumentCaptor<BroadcastReceiver> mReceiverCaptor; @Before public void setUp() throws RemoteException { + // Share class loader to allow access to package-private classes + System.setProperty("dexmaker.share_classloader", "true"); MockitoAnnotations.initMocks(this); doReturn(mContext).when(mContext).createContextAsUser(any(), anyInt()); - mAppHibernationService = new AppHibernationService(mContext, mIPackageManager, - mIActivityManager, mUserManager); + mAppHibernationService = new AppHibernationService(new MockInjector(mContext)); verify(mContext).registerReceiver(mReceiverCaptor.capture(), any()); mBroadcastReceiver = mReceiverCaptor.getValue(); @@ -94,6 +101,12 @@ public final class AppHibernationServiceTest { doAnswer(returnsArgAt(2)).when(mIActivityManager).handleIncomingUser(anyInt(), anyInt(), anyInt(), anyBoolean(), anyBoolean(), any(), any()); + List<PackageInfo> packages = new ArrayList<>(); + packages.add(makePackageInfo(PACKAGE_NAME_1)); + doReturn(new ParceledListSlice<>(packages)).when(mIPackageManager).getInstalledPackages( + intThat(arg -> (arg & MATCH_ANY_USER) != 0), anyInt()); + mAppHibernationService.onBootPhase(SystemService.PHASE_BOOT_COMPLETED); + UserInfo userInfo = addUser(USER_ID_1); mAppHibernationService.onUserUnlocking(new SystemService.TargetUser(userInfo)); doReturn(true).when(mUserManager).isUserUnlockingOrUnlocked(USER_ID_1); @@ -152,7 +165,7 @@ public final class AppHibernationServiceTest { } @Test - public void testSetHibernatingGlobally_packageIsHibernatingGlobally() { + public void testSetHibernatingGlobally_packageIsHibernatingGlobally() throws RemoteException { // WHEN we hibernate a package mAppHibernationService.setHibernatingGlobally(PACKAGE_NAME_1, true); @@ -178,7 +191,7 @@ public final class AppHibernationServiceTest { userPackages.add(makePackageInfo(pkgName)); } doReturn(new ParceledListSlice<>(userPackages)).when(mIPackageManager) - .getInstalledPackages(anyInt(), eq(userId)); + .getInstalledPackages(intThat(arg -> (arg & MATCH_ANY_USER) == 0), eq(userId)); return userInfo; } @@ -187,4 +200,42 @@ public final class AppHibernationServiceTest { pkg.packageName = packageName; return pkg; } + + private class MockInjector implements AppHibernationService.Injector { + private final Context mContext; + + MockInjector(Context context) { + mContext = context; + } + + @Override + public IActivityManager getActivityManager() { + return mIActivityManager; + } + + @Override + public Context getContext() { + return mContext; + } + + @Override + public IPackageManager getPackageManager() { + return mIPackageManager; + } + + @Override + public UserManager getUserManager() { + return mUserManager; + } + + @Override + public HibernationStateDiskStore<GlobalLevelState> getGlobalLevelDiskStore() { + return Mockito.mock(HibernationStateDiskStore.class); + } + + @Override + public HibernationStateDiskStore<UserLevelState> getUserLevelDiskStore(int userId) { + return Mockito.mock(HibernationStateDiskStore.class); + } + } } diff --git a/services/tests/servicestests/src/com/android/server/apphibernation/HibernationStateDiskStoreTest.java b/services/tests/servicestests/src/com/android/server/apphibernation/HibernationStateDiskStoreTest.java new file mode 100644 index 000000000000..59f3c35f2137 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/apphibernation/HibernationStateDiskStoreTest.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2021 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.apphibernation; + +import static org.junit.Assert.assertEquals; + +import android.os.FileUtils; +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoOutputStream; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + + +@SmallTest +public class HibernationStateDiskStoreTest { + private static final String STATES_FILE_NAME = "states"; + private final MockScheduledExecutorService mMockScheduledExecutorService = + new MockScheduledExecutorService(); + + private File mFile; + private HibernationStateDiskStore<String> mHibernationStateDiskStore; + + + @Before + public void setUp() { + mFile = new File(InstrumentationRegistry.getContext().getCacheDir(), "test"); + mHibernationStateDiskStore = new HibernationStateDiskStore<>(mFile, + new MockProtoReadWriter(), mMockScheduledExecutorService, STATES_FILE_NAME); + } + + @After + public void tearDown() { + FileUtils.deleteContentsAndDir(mFile); + } + + @Test + public void testScheduleWriteHibernationStates_writesDataThatCanBeRead() { + // GIVEN some data to be written + List<String> toWrite = new ArrayList<>(Arrays.asList("A", "B")); + + // WHEN the data is written + mHibernationStateDiskStore.scheduleWriteHibernationStates(toWrite); + mMockScheduledExecutorService.executeScheduledTask(); + + // THEN the read data is equal to what was written + List<String> storedStrings = mHibernationStateDiskStore.readHibernationStates(); + for (int i = 0; i < toWrite.size(); i++) { + assertEquals(toWrite.get(i), storedStrings.get(i)); + } + } + + @Test + public void testScheduleWriteHibernationStates_laterWritesOverwritePrevious() { + // GIVEN store has some data it is scheduled to write + mHibernationStateDiskStore.scheduleWriteHibernationStates( + new ArrayList<>(Arrays.asList("C", "D"))); + + // WHEN a write is scheduled with new data + List<String> toWrite = new ArrayList<>(Arrays.asList("A", "B")); + mHibernationStateDiskStore.scheduleWriteHibernationStates(toWrite); + mMockScheduledExecutorService.executeScheduledTask(); + + // THEN the written data is the last scheduled data + List<String> storedStrings = mHibernationStateDiskStore.readHibernationStates(); + for (int i = 0; i < toWrite.size(); i++) { + assertEquals(toWrite.get(i), storedStrings.get(i)); + } + } + + /** + * Mock proto read / writer that just writes and reads a list of String data. + */ + private final class MockProtoReadWriter implements ProtoReadWriter<List<String>> { + private static final long FIELD_ID = 1; + + @Override + public void writeToProto(@NonNull ProtoOutputStream stream, + @NonNull List<String> data) { + for (int i = 0, size = data.size(); i < size; i++) { + stream.write(FIELD_ID, data.get(i)); + } + } + + @Nullable + @Override + public List<String> readFromProto(@NonNull ProtoInputStream stream) + throws IOException { + ArrayList<String> list = new ArrayList<>(); + while (stream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + list.add(stream.readString(FIELD_ID)); + } + return list; + } + } + + /** + * Mock scheduled executor service that has minimum implementation and can synchronously + * execute scheduled tasks. + */ + private final class MockScheduledExecutorService implements ScheduledExecutorService { + + Runnable mScheduledRunnable = null; + + @Override + public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) { + mScheduledRunnable = command; + return Mockito.mock(ScheduledFuture.class); + } + + @Override + public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) { + throw new UnsupportedOperationException(); + } + + @Override + public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, + long period, TimeUnit unit) { + throw new UnsupportedOperationException(); + } + + @Override + public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, + long delay, TimeUnit unit) { + throw new UnsupportedOperationException(); + } + + @Override + public void shutdown() { + throw new UnsupportedOperationException(); + } + + @Override + public List<Runnable> shutdownNow() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isShutdown() { + return false; + } + + @Override + public boolean isTerminated() { + return false; + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override + public <T> Future<T> submit(Callable<T> task) { + throw new UnsupportedOperationException(); + } + + @Override + public <T> Future<T> submit(Runnable task, T result) { + throw new UnsupportedOperationException(); + } + + @Override + public Future<?> submit(Runnable task) { + throw new UnsupportedOperationException(); + } + + @Override + public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) + throws InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override + public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, + TimeUnit unit) throws InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override + public <T> T invokeAny(Collection<? extends Callable<T>> tasks) + throws InterruptedException, ExecutionException { + throw new UnsupportedOperationException(); + } + + @Override + public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + throw new UnsupportedOperationException(); + } + + @Override + public void execute(Runnable command) { + throw new UnsupportedOperationException(); + } + + void executeScheduledTask() { + mScheduledRunnable.run(); + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/powerstats/PowerStatsServiceTest.java b/services/tests/servicestests/src/com/android/server/powerstats/PowerStatsServiceTest.java index 84b690f01b02..53ddb288b4e7 100644 --- a/services/tests/servicestests/src/com/android/server/powerstats/PowerStatsServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/powerstats/PowerStatsServiceTest.java @@ -221,7 +221,7 @@ public class PowerStatsServiceTest { } @Override - public EnergyMeasurement[] readEnergyMeters(int[] channelIds) { + public EnergyMeasurement[] readEnergyMeter(int[] channelIds) { EnergyMeasurement[] energyMeasurementList = new EnergyMeasurement[ENERGY_METER_COUNT]; for (int i = 0; i < energyMeasurementList.length; i++) { energyMeasurementList[i] = new EnergyMeasurement(); |