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);
}