From 4107e3da28b87bacedb6e9af42bea1db6e5cf76f Mon Sep 17 00:00:00 2001 From: Evan Chen Date: Wed, 15 Dec 2021 22:18:20 +0000 Subject: [PATCH] Create a job scheduler for CDM association cleanup A job scheduler that running daily and revoke associations if the selfManaged companion device(EXO) does not connect for 3 months. Fix: 205738945 Test: manually test Change-Id: I6316c8563deb31ef85fd3193cfc6c3f93b8ad5ef --- .../android/companion/AssociationInfo.java | 39 +++++++++- core/res/AndroidManifest.xml | 4 + .../companion/AssociationCleanUpService.java | 77 +++++++++++++++++++ .../CompanionDeviceManagerService.java | 46 ++++++++++- ...CompanionDeviceManagerServiceInternal.java | 29 +++++++ .../server/companion/PersistentDataStore.java | 14 +++- 6 files changed, 201 insertions(+), 8 deletions(-) create mode 100644 services/companion/java/com/android/server/companion/AssociationCleanUpService.java create mode 100644 services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java diff --git a/core/java/android/companion/AssociationInfo.java b/core/java/android/companion/AssociationInfo.java index 78d5137ebfbaa..f0566b856dbdb 100644 --- a/core/java/android/companion/AssociationInfo.java +++ b/core/java/android/companion/AssociationInfo.java @@ -35,6 +35,10 @@ import java.util.Objects; * an app should have access to a companion device. */ public final class AssociationInfo implements Parcelable { + /** + * A String indicates the selfManaged device is not connected. + */ + private static final String LAST_TIME_CONNECTED_NONE = "None"; /** * A unique ID of this Association record. * Disclosed to the clients (ie. companion applications) for referring to this record (eg. in @@ -52,6 +56,11 @@ public final class AssociationInfo implements Parcelable { private final boolean mSelfManaged; private boolean mNotifyOnDeviceNearby; private final long mTimeApprovedMs; + /** + * A long value indicates the last time connected reported by selfManaged devices + * Default value is Long.MAX_VALUE. + */ + private long mLastTimeConnectedMs; /** * Creates a new Association. @@ -62,7 +71,7 @@ public final class AssociationInfo implements Parcelable { public AssociationInfo(int id, @UserIdInt int userId, @NonNull String packageName, @Nullable MacAddress macAddress, @Nullable CharSequence displayName, @Nullable String deviceProfile, boolean selfManaged, boolean notifyOnDeviceNearby, - long timeApprovedMs) { + long timeApprovedMs, long lastTimeConnectedMs) { if (id <= 0) { throw new IllegalArgumentException("Association ID should be greater than 0"); } @@ -83,6 +92,7 @@ public final class AssociationInfo implements Parcelable { mSelfManaged = selfManaged; mNotifyOnDeviceNearby = notifyOnDeviceNearby; mTimeApprovedMs = timeApprovedMs; + mLastTimeConnectedMs = lastTimeConnectedMs; } /** @@ -151,13 +161,21 @@ public final class AssociationInfo implements Parcelable { } /** - * Should only be used by the CdmService. + * Should only be used by the CompanionDeviceManagerService. * @hide */ public void setNotifyOnDeviceNearby(boolean notifyOnDeviceNearby) { mNotifyOnDeviceNearby = notifyOnDeviceNearby; } + /** + * Should only be used by the CompanionDeviceManagerService. + * @hide + */ + public void setLastTimeConnected(long lastTimeConnectedMs) { + mLastTimeConnectedMs = lastTimeConnectedMs; + } + /** @hide */ public boolean isNotifyOnDeviceNearby() { return mNotifyOnDeviceNearby; @@ -173,6 +191,14 @@ public final class AssociationInfo implements Parcelable { return mUserId == userId && Objects.equals(mPackageName, packageName); } + /** + * @return the last time self reported disconnected for selfManaged only. + * @hide + */ + public Long getLastTimeConnectedMs() { + return mLastTimeConnectedMs; + } + /** * Utility method for checking if the association represents a device with the given MAC * address. @@ -223,6 +249,9 @@ public final class AssociationInfo implements Parcelable { + ", mSelfManaged=" + mSelfManaged + ", mNotifyOnDeviceNearby=" + mNotifyOnDeviceNearby + ", mTimeApprovedMs=" + new Date(mTimeApprovedMs) + + ", mLastTimeConnectedMs=" + ( + mLastTimeConnectedMs == Long.MAX_VALUE + ? LAST_TIME_CONNECTED_NONE : new Date(mLastTimeConnectedMs)) + '}'; } @@ -236,6 +265,7 @@ public final class AssociationInfo implements Parcelable { && mSelfManaged == that.mSelfManaged && mNotifyOnDeviceNearby == that.mNotifyOnDeviceNearby && mTimeApprovedMs == that.mTimeApprovedMs + && mLastTimeConnectedMs == that.mLastTimeConnectedMs && Objects.equals(mPackageName, that.mPackageName) && Objects.equals(mDeviceMacAddress, that.mDeviceMacAddress) && Objects.equals(mDisplayName, that.mDisplayName) @@ -245,7 +275,8 @@ public final class AssociationInfo implements Parcelable { @Override public int hashCode() { return Objects.hash(mId, mUserId, mPackageName, mDeviceMacAddress, mDisplayName, - mDeviceProfile, mSelfManaged, mNotifyOnDeviceNearby, mTimeApprovedMs); + mDeviceProfile, mSelfManaged, mNotifyOnDeviceNearby, mTimeApprovedMs, + mLastTimeConnectedMs); } @Override @@ -267,6 +298,7 @@ public final class AssociationInfo implements Parcelable { dest.writeBoolean(mSelfManaged); dest.writeBoolean(mNotifyOnDeviceNearby); dest.writeLong(mTimeApprovedMs); + dest.writeLong(mLastTimeConnectedMs); } private AssociationInfo(@NonNull Parcel in) { @@ -282,6 +314,7 @@ public final class AssociationInfo implements Parcelable { mSelfManaged = in.readBoolean(); mNotifyOnDeviceNearby = in.readBoolean(); mTimeApprovedMs = in.readLong(); + mLastTimeConnectedMs = in.readLong(); } @NonNull diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index d0fee11c5f263..73749a12978ca 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -6590,6 +6590,10 @@ android:permission="android.permission.BIND_JOB_SERVICE"> + + + diff --git a/services/companion/java/com/android/server/companion/AssociationCleanUpService.java b/services/companion/java/com/android/server/companion/AssociationCleanUpService.java new file mode 100644 index 0000000000000..f1d98f09aba3c --- /dev/null +++ b/services/companion/java/com/android/server/companion/AssociationCleanUpService.java @@ -0,0 +1,77 @@ +/* + * 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.companion; + +import static com.android.server.companion.CompanionDeviceManagerService.LOG_TAG; + +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.app.job.JobService; +import android.companion.AssociationRequest; +import android.content.ComponentName; +import android.content.Context; +import android.os.AsyncTask; +import android.util.Slog; + +import com.android.server.LocalServices; + +/** + * A Job Service responsible for clean up the Association. + * The job will be executed only if the device is charging and in idle mode due to the application + * will be killed if association/role are revoked. + */ +public class AssociationCleanUpService extends JobService { + private static final String TAG = LOG_TAG + ".AssociationCleanUpService"; + private static final int JOB_ID = AssociationCleanUpService.class.hashCode(); + private static final long ONE_DAY_INTERVAL = 3 * 24 * 60 * 60 * 1000; // 1 Day + private CompanionDeviceManagerServiceInternal mCdmServiceInternal = LocalServices.getService( + CompanionDeviceManagerServiceInternal.class); + + @Override + public boolean onStartJob(final JobParameters params) { + Slog.i(LOG_TAG, "Execute the Association CleanUp job"); + // Special policy for APP_STREAMING role that need to revoke associations if the device + // does not connect for 3 months. + AsyncTask.execute(() -> { + mCdmServiceInternal.associationCleanUp(AssociationRequest.DEVICE_PROFILE_APP_STREAMING); + jobFinished(params, false); + }); + return true; + } + + @Override + public boolean onStopJob(final JobParameters params) { + Slog.d(TAG, "Association cleanup job stopped; id=" + params.getJobId() + + ", reason=" + + JobParameters.getInternalReasonCodeDescription( + params.getInternalStopReasonCode())); + return false; + } + + static void schedule(Context context) { + Slog.i(LOG_TAG, "Scheduling the Association Cleanup job"); + final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); + final JobInfo job = new JobInfo.Builder(JOB_ID, + new ComponentName(context, AssociationCleanUpService.class)) + .setRequiresCharging(true) + .setRequiresDeviceIdle(true) + .setPeriodic(ONE_DAY_INTERVAL) + .build(); + jobScheduler.schedule(job); + } +} diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 5aa1c933ad873..73b475e08fc76 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -149,6 +149,9 @@ public class CompanionDeviceManagerService extends SystemService private static final String PREF_FILE_NAME = "companion_device_preferences.xml"; private static final String PREF_KEY_AUTO_REVOKE_GRANTS_DONE = "auto_revoke_grants_done"; + private static final long ASSOCIATION_CLEAN_UP_TIME_WINDOW = + 90L * 24 * 60 * 60 * 1000; // 3 months + private static DateFormat sDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); static { sDateFormat.setTimeZone(TimeZone.getDefault()); @@ -179,6 +182,7 @@ public class CompanionDeviceManagerService extends SystemService new ArrayMap<>(); private final RemoteCallbackList mListeners = new RemoteCallbackList<>(); + private final CompanionDeviceManagerServiceInternal mLocalService = new LocalService(this); final Handler mMainHandler = Handler.getMain(); private CompanionDevicePresenceController mCompanionDevicePresenceController; @@ -208,6 +212,8 @@ public class CompanionDeviceManagerService extends SystemService mPermissionControllerManager = requireNonNull( context.getSystemService(PermissionControllerManager.class)); mUserManager = context.getSystemService(UserManager.class); + + LocalServices.addService(CompanionDeviceManagerServiceInternal.class, mLocalService); } @Override @@ -250,6 +256,9 @@ public class CompanionDeviceManagerService extends SystemService } else { Slog.w(LOG_TAG, "No BluetoothAdapter available"); } + } else if (phase == PHASE_BOOT_COMPLETED) { + // Run the Association CleanUp job service daily. + AssociationCleanUpService.schedule(getContext()); } } @@ -294,6 +303,24 @@ public class CompanionDeviceManagerService extends SystemService return association; } + // Revoke associations if the selfManaged companion device does not connect for 3 + // months for specific profile. + private void associationCleanUp(String profile) { + for (AssociationInfo ai : mAssociationStore.getAssociations()) { + if (ai.isSelfManaged() + && profile.equals(ai.getDeviceProfile()) + && System.currentTimeMillis() - ai.getLastTimeConnectedMs() + >= ASSOCIATION_CLEAN_UP_TIME_WINDOW) { + Slog.d(LOG_TAG, "Removing the association for associationId: " + + ai.getId() + + " due to the device does not connect for 3 months." + + " Current time: " + + new Date(System.currentTimeMillis())); + disassociateInternal(ai.getId()); + } + } + } + void maybeGrantAutoRevokeExemptions() { Slog.d(LOG_TAG, "maybeGrantAutoRevokeExemptions()"); PackageManager pm = getContext().getPackageManager(); @@ -573,6 +600,9 @@ public class CompanionDeviceManagerService extends SystemService return; } + association.setLastTimeConnected(System.currentTimeMillis()); + mAssociationStore.updateAssociation(association); + mCompanionDevicePresenceController.onDeviceNotifyAppeared( association, getContext(), mMainHandler); } @@ -735,7 +765,8 @@ public class CompanionDeviceManagerService extends SystemService final long timestamp = System.currentTimeMillis(); final AssociationInfo association = new AssociationInfo(id, userId, packageName, - macAddress, displayName, deviceProfile, selfManaged, false, timestamp); + macAddress, displayName, deviceProfile, selfManaged, false, timestamp, + Long.MAX_VALUE); Slog.i(LOG_TAG, "New CDM association created=" + association); mAssociationStore.addAssociation(association); @@ -1297,4 +1328,17 @@ public class CompanionDeviceManagerService extends SystemService return Collections.unmodifiableMap(copy); } + + private final class LocalService extends CompanionDeviceManagerServiceInternal { + private final CompanionDeviceManagerService mService; + + LocalService(CompanionDeviceManagerService service) { + mService = service; + } + + @Override + public void associationCleanUp(String profile) { + mService.associationCleanUp(profile); + } + } } diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java new file mode 100644 index 0000000000000..326fefe1f8eaf --- /dev/null +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java @@ -0,0 +1,29 @@ +/* + * 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.companion; + +/** + * Companion Device Manager Local System Service Interface. + * + * @hide Only for use within the system server. + */ +public abstract class CompanionDeviceManagerServiceInternal { + /** + * @see CompanionDeviceManagerService#associationCleanUp + */ + public abstract void associationCleanUp(String profile); +} diff --git a/services/companion/java/com/android/server/companion/PersistentDataStore.java b/services/companion/java/com/android/server/companion/PersistentDataStore.java index 97ec3bb7127dd..ef3aa7fea1b54 100644 --- a/services/companion/java/com/android/server/companion/PersistentDataStore.java +++ b/services/companion/java/com/android/server/companion/PersistentDataStore.java @@ -168,6 +168,7 @@ final class PersistentDataStore { private static final String XML_ATTR_SELF_MANAGED = "self_managed"; private static final String XML_ATTR_NOTIFY_DEVICE_NEARBY = "notify_device_nearby"; private static final String XML_ATTR_TIME_APPROVED = "time_approved"; + private static final String XML_ATTR_LAST_TIME_CONNECTED = "last_time_connected"; private static final String LEGACY_XML_ATTR_DEVICE = "device"; @@ -378,7 +379,7 @@ final class PersistentDataStore { out.add(new AssociationInfo(associationId, userId, appPackage, MacAddress.fromString(deviceAddress), null, profile, - /* managedByCompanionApp */false, notify, timeApproved)); + /* managedByCompanionApp */false, notify, timeApproved, Long.MAX_VALUE)); } private static void readAssociationsV1(@NonNull TypedXmlPullParser parser, @@ -408,9 +409,12 @@ final class PersistentDataStore { final boolean selfManaged = readBooleanAttribute(parser, XML_ATTR_SELF_MANAGED); final boolean notify = readBooleanAttribute(parser, XML_ATTR_NOTIFY_DEVICE_NEARBY); final long timeApproved = readLongAttribute(parser, XML_ATTR_TIME_APPROVED, 0L); + final long lastTimeConnected = readLongAttribute( + parser, XML_ATTR_LAST_TIME_CONNECTED, Long.MAX_VALUE); final AssociationInfo associationInfo = createAssociationInfoNoThrow(associationId, userId, - appPackage, macAddress, displayName, profile, selfManaged, notify, timeApproved); + appPackage, macAddress, displayName, profile, selfManaged, notify, timeApproved, + lastTimeConnected); if (associationInfo != null) { out.add(associationInfo); } @@ -464,6 +468,8 @@ final class PersistentDataStore { writeBooleanAttribute( serializer, XML_ATTR_NOTIFY_DEVICE_NEARBY, a.isNotifyOnDeviceNearby()); writeLongAttribute(serializer, XML_ATTR_TIME_APPROVED, a.getTimeApprovedMs()); + writeLongAttribute( + serializer, XML_ATTR_LAST_TIME_CONNECTED, a.getLastTimeConnectedMs()); serializer.endTag(null, XML_TAG_ASSOCIATION); } @@ -512,11 +518,11 @@ final class PersistentDataStore { private static AssociationInfo createAssociationInfoNoThrow(int associationId, @UserIdInt int userId, @NonNull String appPackage, @Nullable MacAddress macAddress, @Nullable CharSequence displayName, @Nullable String profile, boolean selfManaged, - boolean notify, long timeApproved) { + boolean notify, long timeApproved, long lastTimeConnected) { AssociationInfo associationInfo = null; try { associationInfo = new AssociationInfo(associationId, userId, appPackage, macAddress, - displayName, profile, selfManaged, notify, timeApproved); + displayName, profile, selfManaged, notify, timeApproved, lastTimeConnected); } catch (Exception e) { if (DEBUG) Slog.w(LOG_TAG, "Could not create AssociationInfo", e); }