diff options
| author | 2021-02-04 20:18:07 +0000 | |
|---|---|---|
| committer | 2021-02-04 20:18:07 +0000 | |
| commit | de246e1658c65a0a5c46beb917e86fa22aaba3a2 (patch) | |
| tree | 0d990a1c5bc8dd612f21862d88a5b725340f6858 | |
| parent | eff415e7808b41cbb244965dc50f80d0e855a515 (diff) | |
| parent | dba83c49dd7566061d905412e4e6f71103c457af (diff) | |
Merge changes If58d648d,Iec51a349
* changes:
  Add persistence for app hibernation states
  Move AppHibernationServices dependencies to injector
11 files changed, 972 insertions, 60 deletions
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/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/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(); +        } +    } +}  |