diff options
author | 2017-05-10 23:20:35 +0000 | |
---|---|---|
committer | 2017-05-10 23:20:39 +0000 | |
commit | ef5700667fc19a5a903d14e3d90b2d4aed2f2e5c (patch) | |
tree | 4879a319df05e38d848f0b63229d7c21a1490d1b | |
parent | 31ef1394663ff7f36be36e63898544afc6a10c66 (diff) | |
parent | 09c529a9bc85bfd0d50b65f447472ad064eac16c (diff) |
Merge "Reconnect to DAService after binding is dead" into oc-dev
9 files changed, 824 insertions, 35 deletions
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 0840549987a3..5973708aebd2 100755 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -9222,6 +9222,23 @@ public final class Settings { public static final String SHORTCUT_MANAGER_CONSTANTS = "shortcut_manager_constants"; /** + * DevicePolicyManager specific settings. + * This is encoded as a key=value list, separated by commas. Ex: + * + * <pre> + * das_died_service_reconnect_backoff_sec (long) + * das_died_service_reconnect_backoff_increase (float) + * das_died_service_reconnect_max_backoff_sec (long) + * </pre> + * + * <p> + * Type: string + * @hide + * see also com.android.server.devicepolicy.DevicePolicyConstants + */ + public static final String DEVICE_POLICY_CONSTANTS = "device_policy_constants"; + + /** * Get the key that retrieves a bluetooth headset's priority. * @hide */ diff --git a/services/core/java/com/android/server/am/PersistentConnection.java b/services/core/java/com/android/server/am/PersistentConnection.java index c34c097cb696..52eaca10cb4d 100644 --- a/services/core/java/com/android/server/am/PersistentConnection.java +++ b/services/core/java/com/android/server/am/PersistentConnection.java @@ -22,32 +22,77 @@ import android.content.Intent; import android.content.ServiceConnection; import android.os.Handler; import android.os.IBinder; +import android.os.SystemClock; import android.os.UserHandle; import android.util.Slog; +import android.util.TimeUtils; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import java.io.PrintWriter; /** * Connects to a given service component on a given user. * - * - Call {@link #connect()} to create a connection. - * - Call {@link #disconnect()} to disconnect. Make sure to disconnect when the user stops. + * - Call {@link #bind()} to create a connection. + * - Call {@link #unbind()} to disconnect. Make sure to disconnect when the user stops. * * Add onConnected/onDisconnected callbacks as needed. + * + * When the target process gets killed (by OOM-killer, etc), then the activity manager will + * re-connect the connection automatically, in which case onServiceDisconnected() gets called + * and then onServiceConnected(). + * + * However sometimes the activity manager just "kills" the connection -- like when the target + * package gets updated or the target process crashes multiple times in a row, in which case + * onBindingDied() gets called. This class handles this case by re-connecting in the time + * {@link #mRebindBackoffMs}. If this happens again, this class increases the back-off time + * by {@link #mRebindBackoffIncrease} and retry. The back-off time is capped at + * {@link #mRebindMaxBackoffMs}. + * + * The back-off time will never be reset until {@link #unbind()} and {@link #bind()} are called + * explicitly. + * + * NOTE: This class does *not* handle package-updates -- i.e. even if the binding dies due to + * the target package being updated, this class won't reconnect. This is because this class doesn't + * know what to do when the service component has gone missing, for example. If the user of this + * class wants to restore the connection, then it should call {@link #unbind()} and {@link #bind} + * explicitly. */ public abstract class PersistentConnection<T> { private final Object mLock = new Object(); + private final static boolean DEBUG = false; + private final String mTag; private final Context mContext; private final Handler mHandler; private final int mUserId; private final ComponentName mComponentName; + private long mNextBackoffMs; + + private final long mRebindBackoffMs; + private final double mRebindBackoffIncrease; + private final long mRebindMaxBackoffMs; + + private long mReconnectTime; + + // TODO too many booleans... Should clean up. + @GuardedBy("mLock") - private boolean mStarted; + private boolean mBound; + + /** + * Whether {@link #bind()} has been called and {@link #unbind()} hasn't been yet; meaning this + * is the expected bind state from the caller's point of view. + */ + @GuardedBy("mLock") + private boolean mShouldBeBound; + + @GuardedBy("mLock") + private boolean mRebindScheduled; @GuardedBy("mLock") private boolean mIsConnected; @@ -59,6 +104,14 @@ public abstract class PersistentConnection<T> { @Override public void onServiceConnected(ComponentName name, IBinder service) { synchronized (mLock) { + if (!mBound) { + // Callback came in after PersistentConnection.unbind() was called. + // We just ignore this. + // (We've already called unbindService() already in unbind) + Slog.w(mTag, "Connected: " + mComponentName.flattenToShortString() + + " u" + mUserId + " but not bound, ignore."); + return; + } Slog.i(mTag, "Connected: " + mComponentName.flattenToShortString() + " u" + mUserId); @@ -76,15 +129,41 @@ public abstract class PersistentConnection<T> { cleanUpConnectionLocked(); } } + + @Override + public void onBindingDied(ComponentName name) { + // Activity manager gave up; we'll schedule a re-connect by ourselves. + synchronized (mLock) { + if (!mBound) { + // Callback came in late? + Slog.w(mTag, "Binding died: " + mComponentName.flattenToShortString() + + " u" + mUserId + " but not bound, ignore."); + return; + } + + Slog.w(mTag, "Binding died: " + mComponentName.flattenToShortString() + + " u" + mUserId); + scheduleRebindLocked(); + } + } }; + private final Runnable mBindForBackoffRunnable = () -> bindForBackoff(); + public PersistentConnection(@NonNull String tag, @NonNull Context context, - @NonNull Handler handler, int userId, @NonNull ComponentName componentName) { + @NonNull Handler handler, int userId, @NonNull ComponentName componentName, + long rebindBackoffSeconds, double rebindBackoffIncrease, long rebindMaxBackoffSeconds) { mTag = tag; mContext = context; mHandler = handler; mUserId = userId; mComponentName = componentName; + + mRebindBackoffMs = rebindBackoffSeconds * 1000; + mRebindBackoffIncrease = rebindBackoffIncrease; + mRebindMaxBackoffMs = rebindMaxBackoffSeconds * 1000; + + mNextBackoffMs = mRebindBackoffMs; } public final ComponentName getComponentName() { @@ -92,6 +171,27 @@ public abstract class PersistentConnection<T> { } /** + * @return whether {@link #bind()} has been called and {@link #unbind()} hasn't. + * + * Note when the AM gives up on connection, this class detects it and un-bind automatically, + * and schedule rebind, and {@link #isBound} returns false when it's waiting for a retry. + */ + public final boolean isBound() { + synchronized (mLock) { + return mBound; + } + } + + /** + * @return whether re-bind is scheduled after the AM gives up on a connection. + */ + public final boolean isRebindScheduled() { + synchronized (mLock) { + return mRebindScheduled; + } + } + + /** * @return whether connected. */ public final boolean isConnected() { @@ -112,23 +212,51 @@ public abstract class PersistentConnection<T> { /** * Connects to the service. */ - public final void connect() { + public final void bind() { synchronized (mLock) { - if (mStarted) { - return; - } - mStarted = true; + mShouldBeBound = true; - final Intent service = new Intent().setComponent(mComponentName); + bindInnerLocked(/* resetBackoff= */ true); + } + } + + public final void bindInnerLocked(boolean resetBackoff) { + unscheduleRebindLocked(); + + if (mBound) { + return; + } + mBound = true; + + if (resetBackoff) { + // Note this is the only place we reset the backoff time. + mNextBackoffMs = mRebindBackoffMs; + } - final boolean success = mContext.bindServiceAsUser(service, mServiceConnection, - Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE, - mHandler, UserHandle.of(mUserId)); + final Intent service = new Intent().setComponent(mComponentName); - if (!success) { - Slog.e(mTag, "Binding: " + service.getComponent() + " u" + mUserId - + " failed."); + if (DEBUG) { + Slog.d(mTag, "Attempting to connect to " + mComponentName); + } + + final boolean success = mContext.bindServiceAsUser(service, mServiceConnection, + Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE, + mHandler, UserHandle.of(mUserId)); + + if (!success) { + Slog.e(mTag, "Binding: " + service.getComponent() + " u" + mUserId + + " failed."); + } + } + + final void bindForBackoff() { + synchronized (mLock) { + if (!mShouldBeBound) { + // Race condition -- by the time we got here, unbind() has already been called. + return; } + + bindInnerLocked(/* resetBackoff= */ false); } } @@ -140,16 +268,46 @@ public abstract class PersistentConnection<T> { /** * Disconnect from the service. */ - public final void disconnect() { + public final void unbind() { synchronized (mLock) { - if (!mStarted) { - return; - } - Slog.i(mTag, "Stopping: " + mComponentName.flattenToShortString() + " u" + mUserId); - mStarted = false; - mContext.unbindService(mServiceConnection); + mShouldBeBound = false; - cleanUpConnectionLocked(); + unbindLocked(); + } + } + + private final void unbindLocked() { + unscheduleRebindLocked(); + + if (!mBound) { + return; + } + Slog.i(mTag, "Stopping: " + mComponentName.flattenToShortString() + " u" + mUserId); + mBound = false; + mContext.unbindService(mServiceConnection); + + cleanUpConnectionLocked(); + } + + void unscheduleRebindLocked() { + injectRemoveCallbacks(mBindForBackoffRunnable); + mRebindScheduled = false; + } + + void scheduleRebindLocked() { + unbindLocked(); + + if (!mRebindScheduled) { + Slog.i(mTag, "Scheduling to reconnect in " + mNextBackoffMs + " ms (uptime)"); + + mReconnectTime = injectUptimeMillis() + mNextBackoffMs; + + injectPostAtTime(mBindForBackoffRunnable, mReconnectTime); + + mNextBackoffMs = Math.min(mRebindMaxBackoffMs, + (long) (mNextBackoffMs * mRebindBackoffIncrease)); + + mRebindScheduled = true; } } @@ -160,9 +318,57 @@ public abstract class PersistentConnection<T> { synchronized (mLock) { pw.print(prefix); pw.print(mComponentName.flattenToShortString()); - pw.print(mStarted ? " [started]" : " [not started]"); + pw.print(mBound ? " [bound]" : " [not bound]"); pw.print(mIsConnected ? " [connected]" : " [not connected]"); + if (mRebindScheduled) { + pw.print(" reconnect in "); + TimeUtils.formatDuration((mReconnectTime - injectUptimeMillis()), pw); + } pw.println(); + + pw.print(prefix); + pw.print(" Next backoff(sec): "); + pw.print(mNextBackoffMs / 1000); } } + + @VisibleForTesting + void injectRemoveCallbacks(Runnable r) { + mHandler.removeCallbacks(r); + } + + @VisibleForTesting + void injectPostAtTime(Runnable r, long uptimeMillis) { + mHandler.postAtTime(r, uptimeMillis); + } + + @VisibleForTesting + long injectUptimeMillis() { + return SystemClock.uptimeMillis(); + } + + @VisibleForTesting + long getNextBackoffMsForTest() { + return mNextBackoffMs; + } + + @VisibleForTesting + long getReconnectTimeForTest() { + return mReconnectTime; + } + + @VisibleForTesting + ServiceConnection getServiceConnectionForTest() { + return mServiceConnection; + } + + @VisibleForTesting + Runnable getBindForBackoffRunnableForTest() { + return mBindForBackoffRunnable; + } + + @VisibleForTesting + boolean shouldBeBoundForTest() { + return mShouldBeBound; + } } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DeviceAdminServiceController.java b/services/devicepolicy/java/com/android/server/devicepolicy/DeviceAdminServiceController.java index c7b8f029e4fc..60f204dd4bbc 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DeviceAdminServiceController.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DeviceAdminServiceController.java @@ -53,6 +53,7 @@ public class DeviceAdminServiceController { private final DevicePolicyManagerService mService; private final DevicePolicyManagerService.Injector mInjector; + private final DevicePolicyConstants mConstants; private final Handler mHandler; // needed? @@ -66,7 +67,10 @@ public class DeviceAdminServiceController { private class DevicePolicyServiceConnection extends PersistentConnection<IDeviceAdminService> { public DevicePolicyServiceConnection(int userId, @NonNull ComponentName componentName) { - super(TAG, mContext, mHandler, userId, componentName); + super(TAG, mContext, mHandler, userId, componentName, + mConstants.DAS_DIED_SERVICE_RECONNECT_BACKOFF_SEC, + mConstants.DAS_DIED_SERVICE_RECONNECT_BACKOFF_INCREASE, + mConstants.DAS_DIED_SERVICE_RECONNECT_MAX_BACKOFF_SEC); } @Override @@ -81,11 +85,13 @@ public class DeviceAdminServiceController { @GuardedBy("mLock") private final SparseArray<DevicePolicyServiceConnection> mConnections = new SparseArray<>(); - public DeviceAdminServiceController(DevicePolicyManagerService service) { + public DeviceAdminServiceController(DevicePolicyManagerService service, + DevicePolicyConstants constants) { mService = service; mInjector = service.mInjector; mContext = mInjector.mContext; mHandler = new Handler(BackgroundThread.get().getLooper()); + mConstants = constants; } /** @@ -150,9 +156,11 @@ public class DeviceAdminServiceController { final PersistentConnection<IDeviceAdminService> existing = mConnections.get(userId); if (existing != null) { - if (existing.getComponentName().equals(service.getComponentName())) { - return; - } + // Note even when we're already connected to the same service, the binding + // would have died at this point due to a package update. So we disconnect + // anyway and re-connect. + debug("Disconnecting from existing service connection.", + packageName, userId); disconnectServiceOnUserLocked(userId, actionForLog); } @@ -164,7 +172,7 @@ public class DeviceAdminServiceController { new DevicePolicyServiceConnection( userId, service.getComponentName()); mConnections.put(userId, conn); - conn.connect(); + conn.bind(); } } finally { mInjector.binderRestoreCallingIdentity(token); @@ -190,7 +198,7 @@ public class DeviceAdminServiceController { if (conn != null) { debug("Stopping service for u%d if already running for %s.", userId, actionForLog); - conn.disconnect(); + conn.unbind(); mConnections.remove(userId); } } @@ -209,6 +217,7 @@ public class DeviceAdminServiceController { final DevicePolicyServiceConnection con = mConnections.valueAt(i); con.dump(prefix + " ", pw); } + pw.println(); } } } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyConstants.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyConstants.java new file mode 100644 index 000000000000..616c669de939 --- /dev/null +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyConstants.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2017 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.devicepolicy; + +import android.util.KeyValueListParser; +import android.util.Slog; + +import java.io.PrintWriter; +import java.util.concurrent.TimeUnit; + +/** + * Constants that are configurable via the global settings for {@link DevicePolicyManagerService}. + * + * Example of setting the values for testing. + * adb shell settings put global device_policy_constants das_died_service_reconnect_backoff_sec=10,das_died_service_reconnect_backoff_increase=1.5,das_died_service_reconnect_max_backoff_sec=30 + */ +public class DevicePolicyConstants { + private static final String TAG = DevicePolicyManagerService.LOG_TAG; + + private static final String DAS_DIED_SERVICE_RECONNECT_BACKOFF_SEC_KEY + = "das_died_service_reconnect_backoff_sec"; + + private static final String DAS_DIED_SERVICE_RECONNECT_BACKOFF_INCREASE_KEY + = "das_died_service_reconnect_backoff_increase"; + + private static final String DAS_DIED_SERVICE_RECONNECT_MAX_BACKOFF_SEC_KEY + = "das_died_service_reconnect_max_backoff_sec"; + + /** + * The back-off before re-connecting, when a service binding died, due to the owner + * crashing repeatedly. + */ + public final long DAS_DIED_SERVICE_RECONNECT_BACKOFF_SEC; + + /** + * The exponential back-off increase factor when a binding dies multiple times. + */ + public final double DAS_DIED_SERVICE_RECONNECT_BACKOFF_INCREASE; + + /** + * The max back-off + */ + public final long DAS_DIED_SERVICE_RECONNECT_MAX_BACKOFF_SEC; + + private DevicePolicyConstants(String settings) { + + final KeyValueListParser parser = new KeyValueListParser(','); + try { + parser.setString(settings); + } catch (IllegalArgumentException e) { + // Failed to parse the settings string, log this and move on + // with defaults. + Slog.e(TAG, "Bad device policy settings: " + settings); + } + + long dasDiedServiceReconnectBackoffSec = parser.getLong( + DAS_DIED_SERVICE_RECONNECT_BACKOFF_SEC_KEY, TimeUnit.HOURS.toSeconds(1)); + + double dasDiedServiceReconnectBackoffIncrease = parser.getFloat( + DAS_DIED_SERVICE_RECONNECT_BACKOFF_INCREASE_KEY, 2f); + + long dasDiedServiceReconnectMaxBackoffSec = parser.getLong( + DAS_DIED_SERVICE_RECONNECT_MAX_BACKOFF_SEC_KEY, TimeUnit.DAYS.toSeconds(1)); + + // Set minimum: 5 seconds. + dasDiedServiceReconnectBackoffSec = Math.max(5, dasDiedServiceReconnectBackoffSec); + + // Set minimum: 1.0. + dasDiedServiceReconnectBackoffIncrease = + Math.max(1, dasDiedServiceReconnectBackoffIncrease); + + // Make sure max >= default back off. + dasDiedServiceReconnectMaxBackoffSec = Math.max(dasDiedServiceReconnectBackoffSec, + dasDiedServiceReconnectMaxBackoffSec); + + DAS_DIED_SERVICE_RECONNECT_BACKOFF_SEC = dasDiedServiceReconnectBackoffSec; + DAS_DIED_SERVICE_RECONNECT_BACKOFF_INCREASE = dasDiedServiceReconnectBackoffIncrease; + DAS_DIED_SERVICE_RECONNECT_MAX_BACKOFF_SEC = dasDiedServiceReconnectMaxBackoffSec; + + } + + public static DevicePolicyConstants loadFromString(String settings) { + return new DevicePolicyConstants(settings); + } + + public void dump(String prefix, PrintWriter pw) { + pw.print(prefix); + pw.println("Constants:"); + + pw.print(prefix); + pw.print(" DAS_DIED_SERVICE_RECONNECT_BACKOFF_SEC: "); + pw.println( DAS_DIED_SERVICE_RECONNECT_BACKOFF_SEC); + + pw.print(prefix); + pw.print(" DAS_DIED_SERVICE_RECONNECT_BACKOFF_INCREASE: "); + pw.println( DAS_DIED_SERVICE_RECONNECT_BACKOFF_INCREASE); + + pw.print(prefix); + pw.print(" DAS_DIED_SERVICE_RECONNECT_MAX_BACKOFF_SEC: "); + pw.println( DAS_DIED_SERVICE_RECONNECT_MAX_BACKOFF_SEC); + } +} diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 87fb8c8dd272..911bb2a70173 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -141,6 +141,7 @@ import android.os.storage.StorageManager; import android.provider.ContactsContract.QuickContact; import android.provider.ContactsInternal; import android.provider.Settings; +import android.provider.Settings.Global; import android.security.IKeyChainAliasCallback; import android.security.IKeyChainService; import android.security.KeyChain; @@ -364,6 +365,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { final UserManagerInternal mUserManagerInternal; final TelephonyManager mTelephonyManager; private final LockPatternUtils mLockPatternUtils; + private final DevicePolicyConstants mConstants; private final DeviceAdminServiceController mDeviceAdminServiceController; /** @@ -1447,7 +1449,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { private void handlePackagesChanged(@Nullable String packageName, int userHandle) { boolean removedAdmin = false; - if (VERBOSE_LOG) Slog.d(LOG_TAG, "Handling package changes for user " + userHandle); + if (VERBOSE_LOG) { + Slog.d(LOG_TAG, "Handling package changes package " + packageName + + " for user " + userHandle); + } DevicePolicyData policy = getUserData(userHandle); synchronized (this) { for (int i = policy.mAdminList.size() - 1; i >= 0; i--) { @@ -1760,6 +1765,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return Settings.Global.getInt(mContext.getContentResolver(), name, def); } + String settingsGlobalGetString(String name) { + return Settings.Global.getString(mContext.getContentResolver(), name); + } + void settingsGlobalPutInt(String name, int value) { Settings.Global.putInt(mContext.getContentResolver(), name, value); } @@ -1801,6 +1810,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { mInjector = injector; mContext = Preconditions.checkNotNull(injector.mContext); mHandler = new Handler(Preconditions.checkNotNull(injector.getMyLooper())); + mConstants = DevicePolicyConstants.loadFromString( + mInjector.settingsGlobalGetString(Global.DEVICE_POLICY_CONSTANTS)); + mOwners = Preconditions.checkNotNull(injector.newOwners()); mUserManager = Preconditions.checkNotNull(injector.getUserManager()); @@ -1823,7 +1835,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { // Needed when mHasFeature == false, because it controls the certificate warning text. mCertificateMonitor = new CertificateMonitor(this, mInjector, mBackgroundHandler); - mDeviceAdminServiceController = new DeviceAdminServiceController(this); + mDeviceAdminServiceController = new DeviceAdminServiceController(this, mConstants); if (!mHasFeature) { // Skip the rest of the initialization @@ -7354,6 +7366,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { synchronized (this) { pw.println("Current Device Policy Manager state:"); + mOwners.dump(" ", pw); mDeviceAdminServiceController.dump(" ", pw); int userCount = mUserData.size(); @@ -7380,7 +7393,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { pw.print(" mPasswordOwner="); pw.println(policy.mPasswordOwner); } pw.println(); - pw.println("Encryption Status: " + getEncryptionStatusName(getEncryptionStatus())); + mConstants.dump(" ", pw); + pw.println(); + pw.println(" Encryption Status: " + getEncryptionStatusName(getEncryptionStatus())); } } diff --git a/services/tests/servicestests/src/com/android/server/am/PersistentConnectionTest.java b/services/tests/servicestests/src/com/android/server/am/PersistentConnectionTest.java new file mode 100644 index 000000000000..f28738604fd5 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/am/PersistentConnectionTest.java @@ -0,0 +1,352 @@ +/* + * Copyright (C) 2017 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.am; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.admin.IDeviceAdminService; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.UserHandle; +import android.test.AndroidTestCase; +import android.util.Pair; + +import org.mockito.ArgumentMatchers; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; + +public class PersistentConnectionTest extends AndroidTestCase { + private static class MyConnection extends PersistentConnection<IDeviceAdminService> { + public long uptimeMillis = 12345; + + public ArrayList<Pair<Runnable, Long>> scheduledRunnables = new ArrayList<>(); + + public MyConnection(String tag, Context context, Handler handler, int userId, + ComponentName componentName, long rebindBackoffSeconds, + double rebindBackoffIncrease, long rebindMaxBackoffSeconds) { + super(tag, context, handler, userId, componentName, + rebindBackoffSeconds, rebindBackoffIncrease, rebindMaxBackoffSeconds); + } + + @Override + protected IDeviceAdminService asInterface(IBinder binder) { + return (IDeviceAdminService) binder; + } + + @Override + long injectUptimeMillis() { + return uptimeMillis; + } + + @Override + void injectPostAtTime(Runnable r, long uptimeMillis) { + scheduledRunnables.add(Pair.create(r, uptimeMillis)); + } + + @Override + void injectRemoveCallbacks(Runnable r) { + for (int i = scheduledRunnables.size() - 1; i >= 0; i--) { + if (scheduledRunnables.get(i).first.equals(r)) { + scheduledRunnables.remove(i); + } + } + } + + void elapse(long milliSeconds) { + uptimeMillis += milliSeconds; + + // Fire the scheduled runnables. + + // Note we collect first and then run all, because sometimes a scheduled runnable + // calls removeCallbacks. + final ArrayList<Runnable> list = new ArrayList<>(); + + for (int i = scheduledRunnables.size() - 1; i >= 0; i--) { + if (scheduledRunnables.get(i).second <= uptimeMillis) { + list.add(scheduledRunnables.get(i).first); + scheduledRunnables.remove(i); + } + } + + Collections.reverse(list); + for (Runnable r : list) { + r.run(); + } + } + } + + public void testAll() { + final Context context = mock(Context.class); + final int userId = 11; + final ComponentName cn = ComponentName.unflattenFromString("a.b.c/def"); + final Handler handler = new Handler(Looper.getMainLooper()); + + final MyConnection conn = new MyConnection("tag", context, handler, userId, cn, + /* rebindBackoffSeconds= */ 5, + /* rebindBackoffIncrease= */ 1.5, + /* rebindMaxBackoffSeconds= */ 11); + + assertFalse(conn.isBound()); + assertFalse(conn.isConnected()); + assertFalse(conn.isRebindScheduled()); + assertEquals(5000, conn.getNextBackoffMsForTest()); + assertNull(conn.getServiceBinder()); + + when(context.bindServiceAsUser(any(Intent.class), any(ServiceConnection.class), anyInt(), + any(Handler.class), any(UserHandle.class))) + .thenReturn(true); + + // Call bind. + conn.bind(); + + assertTrue(conn.isBound()); + assertTrue(conn.shouldBeBoundForTest()); + assertFalse(conn.isConnected()); + assertFalse(conn.isRebindScheduled()); + assertNull(conn.getServiceBinder()); + + assertEquals(5000, conn.getNextBackoffMsForTest()); + + verify(context).bindServiceAsUser( + ArgumentMatchers.argThat(intent -> cn.equals(intent.getComponent())), + eq(conn.getServiceConnectionForTest()), + eq(Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE), + eq(handler), eq(UserHandle.of(userId))); + + // AM responds... + conn.getServiceConnectionForTest().onServiceConnected(cn, + new IDeviceAdminService.Stub() {}); + + assertTrue(conn.isBound()); + assertTrue(conn.shouldBeBoundForTest()); + assertTrue(conn.isConnected()); + assertNotNull(conn.getServiceBinder()); + assertFalse(conn.isRebindScheduled()); + + assertEquals(5000, conn.getNextBackoffMsForTest()); + + + // Now connected. + + // Call unbind... + conn.unbind(); + assertFalse(conn.isBound()); + assertFalse(conn.shouldBeBoundForTest()); + assertFalse(conn.isConnected()); + assertNull(conn.getServiceBinder()); + assertFalse(conn.isRebindScheduled()); + + // Caller bind again... + conn.bind(); + + assertTrue(conn.isBound()); + assertTrue(conn.shouldBeBoundForTest()); + assertFalse(conn.isConnected()); + assertFalse(conn.isRebindScheduled()); + assertNull(conn.getServiceBinder()); + + assertEquals(5000, conn.getNextBackoffMsForTest()); + + + // Now connected again. + + // The service got killed... + conn.getServiceConnectionForTest().onServiceDisconnected(cn); + + assertTrue(conn.isBound()); + assertTrue(conn.shouldBeBoundForTest()); + assertFalse(conn.isConnected()); + assertNull(conn.getServiceBinder()); + assertFalse(conn.isRebindScheduled()); + + assertEquals(5000, conn.getNextBackoffMsForTest()); + + // Connected again... + conn.getServiceConnectionForTest().onServiceConnected(cn, + new IDeviceAdminService.Stub() {}); + + assertTrue(conn.isBound()); + assertTrue(conn.shouldBeBoundForTest()); + assertTrue(conn.isConnected()); + assertNotNull(conn.getServiceBinder()); + assertFalse(conn.isRebindScheduled()); + + assertEquals(5000, conn.getNextBackoffMsForTest()); + + + // Then the binding is "died"... + conn.getServiceConnectionForTest().onBindingDied(cn); + + assertFalse(conn.isBound()); + assertTrue(conn.shouldBeBoundForTest()); + assertFalse(conn.isConnected()); + assertNull(conn.getServiceBinder()); + assertTrue(conn.isRebindScheduled()); + + assertEquals(7500, conn.getNextBackoffMsForTest()); + + assertEquals( + Arrays.asList(Pair.create(conn.getBindForBackoffRunnableForTest(), + conn.uptimeMillis + 5000)), + conn.scheduledRunnables); + + // 5000 ms later... + conn.elapse(5000); + + assertTrue(conn.isBound()); + assertTrue(conn.shouldBeBoundForTest()); + assertFalse(conn.isConnected()); + assertNull(conn.getServiceBinder()); + assertFalse(conn.isRebindScheduled()); + + assertEquals(7500, conn.getNextBackoffMsForTest()); + + // Connected. + conn.getServiceConnectionForTest().onServiceConnected(cn, + new IDeviceAdminService.Stub() {}); + + assertTrue(conn.isBound()); + assertTrue(conn.shouldBeBoundForTest()); + assertTrue(conn.isConnected()); + assertNotNull(conn.getServiceBinder()); + assertFalse(conn.isRebindScheduled()); + + assertEquals(7500, conn.getNextBackoffMsForTest()); + + // Then the binding is "died"... + conn.getServiceConnectionForTest().onBindingDied(cn); + + assertFalse(conn.isBound()); + assertTrue(conn.shouldBeBoundForTest()); + assertFalse(conn.isConnected()); + assertNull(conn.getServiceBinder()); + assertTrue(conn.isRebindScheduled()); + + assertEquals(11000, conn.getNextBackoffMsForTest()); + + assertEquals( + Arrays.asList(Pair.create(conn.getBindForBackoffRunnableForTest(), + conn.uptimeMillis + 7500)), + conn.scheduledRunnables); + + // Later... + conn.elapse(7500); + + assertTrue(conn.isBound()); + assertTrue(conn.shouldBeBoundForTest()); + assertFalse(conn.isConnected()); + assertNull(conn.getServiceBinder()); + assertFalse(conn.isRebindScheduled()); + + assertEquals(11000, conn.getNextBackoffMsForTest()); + + + // Then the binding is "died"... + conn.getServiceConnectionForTest().onBindingDied(cn); + + assertFalse(conn.isBound()); + assertTrue(conn.shouldBeBoundForTest()); + assertFalse(conn.isConnected()); + assertNull(conn.getServiceBinder()); + assertTrue(conn.isRebindScheduled()); + + assertEquals(11000, conn.getNextBackoffMsForTest()); + + assertEquals( + Arrays.asList(Pair.create(conn.getBindForBackoffRunnableForTest(), + conn.uptimeMillis + 11000)), + conn.scheduledRunnables); + + // Call unbind... + conn.unbind(); + assertFalse(conn.isBound()); + assertFalse(conn.shouldBeBoundForTest()); + assertFalse(conn.isConnected()); + assertNull(conn.getServiceBinder()); + assertFalse(conn.isRebindScheduled()); + + // Call bind again... And now the backoff is reset to 5000. + conn.bind(); + + assertTrue(conn.isBound()); + assertTrue(conn.shouldBeBoundForTest()); + assertFalse(conn.isConnected()); + assertFalse(conn.isRebindScheduled()); + assertNull(conn.getServiceBinder()); + + assertEquals(5000, conn.getNextBackoffMsForTest()); + } + + public void testReconnectFiresAfterUnbind() { + final Context context = mock(Context.class); + final int userId = 11; + final ComponentName cn = ComponentName.unflattenFromString("a.b.c/def"); + final Handler handler = new Handler(Looper.getMainLooper()); + + final MyConnection conn = new MyConnection("tag", context, handler, userId, cn, + /* rebindBackoffSeconds= */ 5, + /* rebindBackoffIncrease= */ 1.5, + /* rebindMaxBackoffSeconds= */ 11); + + when(context.bindServiceAsUser(any(Intent.class), any(ServiceConnection.class), anyInt(), + any(Handler.class), any(UserHandle.class))) + .thenReturn(true); + + // Bind. + conn.bind(); + + assertTrue(conn.isBound()); + assertTrue(conn.shouldBeBoundForTest()); + assertFalse(conn.isRebindScheduled()); + + conn.elapse(1000); + + // Service crashes. + conn.getServiceConnectionForTest().onBindingDied(cn); + + assertFalse(conn.isBound()); + assertTrue(conn.shouldBeBoundForTest()); + assertTrue(conn.isRebindScheduled()); + + assertEquals(7500, conn.getNextBackoffMsForTest()); + + // Call unbind. + conn.unbind(); + assertFalse(conn.isBound()); + assertFalse(conn.shouldBeBoundForTest()); + + // Now, at this point, it's possible that the scheduled runnable had already been fired + // before during the unbind() call, and waiting on mLock. + // To simulate it, we just call the runnable here. + conn.getBindForBackoffRunnableForTest().run(); + + // Should still not be bound. + assertFalse(conn.isBound()); + assertFalse(conn.shouldBeBoundForTest()); + } +} diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyConstantsTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyConstantsTest.java new file mode 100644 index 000000000000..3819914fe9b6 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyConstantsTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2017 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.devicepolicy; + +import android.test.AndroidTestCase; + +/** + * Test for {@link DevicePolicyConstants}. + * + m FrameworksServicesTests && + adb install \ + -r ${ANDROID_PRODUCT_OUT}/data/app/FrameworksServicesTests/FrameworksServicesTests.apk && + adb shell am instrument -e class com.android.server.devicepolicy.DevicePolicyConstantsTest \ + -w com.android.frameworks.servicestests + + + -w com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner + */ +public class DevicePolicyConstantsTest extends AndroidTestCase { + private static final String TAG = "DevicePolicyConstantsTest"; + + public void testDefaultValues() throws Exception { + final DevicePolicyConstants constants = DevicePolicyConstants.loadFromString(""); + + assertEquals(1 * 60 * 60, constants.DAS_DIED_SERVICE_RECONNECT_BACKOFF_SEC); + assertEquals(24 * 60 * 60, constants.DAS_DIED_SERVICE_RECONNECT_MAX_BACKOFF_SEC); + assertEquals(2.0, constants.DAS_DIED_SERVICE_RECONNECT_BACKOFF_INCREASE); + } + + public void testCustomValues() throws Exception { + final DevicePolicyConstants constants = DevicePolicyConstants.loadFromString( + "das_died_service_reconnect_backoff_sec=10," + + "das_died_service_reconnect_backoff_increase=1.25," + + "das_died_service_reconnect_max_backoff_sec=15" + ); + + assertEquals(10, constants.DAS_DIED_SERVICE_RECONNECT_BACKOFF_SEC); + assertEquals(15, constants.DAS_DIED_SERVICE_RECONNECT_MAX_BACKOFF_SEC); + assertEquals(1.25, constants.DAS_DIED_SERVICE_RECONNECT_BACKOFF_INCREASE); + } + + public void testMinMax() throws Exception { + final DevicePolicyConstants constants = DevicePolicyConstants.loadFromString( + "das_died_service_reconnect_backoff_sec=3," + + "das_died_service_reconnect_backoff_increase=.25," + + "das_died_service_reconnect_max_backoff_sec=1" + ); + + assertEquals(5, constants.DAS_DIED_SERVICE_RECONNECT_BACKOFF_SEC); + assertEquals(5, constants.DAS_DIED_SERVICE_RECONNECT_MAX_BACKOFF_SEC); + assertEquals(1.0, constants.DAS_DIED_SERVICE_RECONNECT_BACKOFF_INCREASE); + } +} diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java index 46da3dedc77e..b870d9404181 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java @@ -369,6 +369,11 @@ public class DevicePolicyManagerServiceTestable extends DevicePolicyManagerServi } @Override + String settingsGlobalGetString(String name) { + return context.settings.settingsGlobalGetString(name); + } + + @Override void securityLogSetLoggingEnabledProperty(boolean enabled) { context.settings.securityLogSetLoggingEnabledProperty(enabled); } diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java index 23fada46d16e..87106ec7b918 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java @@ -233,6 +233,10 @@ public class DpmMockContext extends MockContext { return 0; } + public String settingsGlobalGetString(String name) { + return ""; + } + public void securityLogSetLoggingEnabledProperty(boolean enabled) { } |