/* * Copyright (C) 2019 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 android.companion; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.TestApi; import android.annotation.UserIdInt; import android.net.MacAddress; import android.os.Parcel; import android.os.Parcelable; import java.util.Date; import java.util.Objects; /** * Details for a specific "association" that has been established between an app and companion * device. *

* An association gives an app the ability to interact with a companion device without needing to * acquire broader runtime permissions. An association only exists after the user has confirmed that * 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 (i.e. companion applications) for referring to this record (e.g. in * {@code disassociate()} API call). */ private final int mId; @UserIdInt private final int mUserId; @NonNull private final String mPackageName; @Nullable private final String mTag; @Nullable private final MacAddress mDeviceMacAddress; @Nullable private final CharSequence mDisplayName; @Nullable private final String mDeviceProfile; @Nullable private final AssociatedDevice mAssociatedDevice; private final boolean mSelfManaged; private final boolean mNotifyOnDeviceNearby; /** * Indicates that the association has been revoked (removed), but we keep the association * record for final clean up (e.g. removing the app from the list of the role holders). * * @see CompanionDeviceManager#disassociate(int) */ private final boolean mRevoked; /** * Indicates that the association is waiting for its corresponding companion app to be installed * before it can be added to CDM. This is likely because it was restored onto the device from a * backup. */ private final boolean mPending; private final long mTimeApprovedMs; /** * A long value indicates the last time connected reported by selfManaged devices * Default value is Long.MAX_VALUE. */ private final long mLastTimeConnectedMs; private final int mSystemDataSyncFlags; /** * Creates a new Association. * * @hide */ public AssociationInfo(int id, @UserIdInt int userId, @NonNull String packageName, @Nullable String tag, @Nullable MacAddress macAddress, @Nullable CharSequence displayName, @Nullable String deviceProfile, @Nullable AssociatedDevice associatedDevice, boolean selfManaged, boolean notifyOnDeviceNearby, boolean revoked, boolean pending, long timeApprovedMs, long lastTimeConnectedMs, int systemDataSyncFlags) { if (id <= 0) { throw new IllegalArgumentException("Association ID should be greater than 0"); } if (macAddress == null && displayName == null) { throw new IllegalArgumentException("MAC address and the Display Name must NOT be null " + "at the same time"); } mId = id; mUserId = userId; mPackageName = packageName; mDeviceMacAddress = macAddress; mDisplayName = displayName; mTag = tag; mDeviceProfile = deviceProfile; mAssociatedDevice = associatedDevice; mSelfManaged = selfManaged; mNotifyOnDeviceNearby = notifyOnDeviceNearby; mRevoked = revoked; mPending = pending; mTimeApprovedMs = timeApprovedMs; mLastTimeConnectedMs = lastTimeConnectedMs; mSystemDataSyncFlags = systemDataSyncFlags; } /** * @return the unique ID of this association record. */ public int getId() { return mId; } /** * @return the ID of the user who "owns" this association. * @hide */ @UserIdInt public int getUserId() { return mUserId; } /** * @return the package name of the app which this association refers to. * @hide */ @SystemApi @NonNull public String getPackageName() { return mPackageName; } /** * @return the tag of this association. * @see CompanionDeviceManager#setAssociationTag(int, String) */ @FlaggedApi(Flags.FLAG_ASSOCIATION_TAG) @Nullable public String getTag() { return mTag; } /** * @return the MAC address of the device. */ @Nullable public MacAddress getDeviceMacAddress() { return mDeviceMacAddress; } /** @hide */ @Nullable public String getDeviceMacAddressAsString() { return mDeviceMacAddress != null ? mDeviceMacAddress.toString().toUpperCase() : null; } /** * @return the display name of the companion device (optionally) provided by the companion * application. * * @see AssociationRequest.Builder#setDisplayName(CharSequence) */ @Nullable public CharSequence getDisplayName() { return mDisplayName; } /** * @return the companion device profile used when establishing this * association, or {@code null} if no specific profile was used. * @see AssociationRequest.Builder#setDeviceProfile(String) */ @Nullable public String getDeviceProfile() { return mDeviceProfile; } /** * Companion device that was associated. Note that this field is not persisted across sessions. * Device can be one of the following types: * *

* * @return the companion device that was associated, or {@code null} if the device is * self-managed or this association info was retrieved from persistent storage. */ @Nullable public AssociatedDevice getAssociatedDevice() { return mAssociatedDevice; } /** * @return whether the association is managed by the companion application it belongs to. * @see AssociationRequest.Builder#setSelfManaged(boolean) */ @SuppressLint("UnflaggedApi") // promoting from @SystemApi public boolean isSelfManaged() { return mSelfManaged; } /** @hide */ public boolean isNotifyOnDeviceNearby() { return mNotifyOnDeviceNearby; } /** @hide */ public long getTimeApprovedMs() { return mTimeApprovedMs; } /** @hide */ public boolean belongsToPackage(@UserIdInt int userId, String packageName) { return mUserId == userId && Objects.equals(mPackageName, packageName); } /** * @return if the association has been revoked (removed). * @hide */ public boolean isRevoked() { return mRevoked; } /** * @return true if the association is waiting for its corresponding app to be installed * before it can be added to CDM. * @hide */ public boolean isPending() { return mPending; } /** * @return true if the association is not revoked nor pending * @hide */ public boolean isActive() { return !mRevoked && !mPending; } /** * @return the last time self reported disconnected for selfManaged only. * @hide */ public long getLastTimeConnectedMs() { return mLastTimeConnectedMs; } /** * @return Enabled system data sync flags set via * {@link CompanionDeviceManager#enableSystemDataSyncForTypes(int, int)} (int, int)} and * {@link CompanionDeviceManager#disableSystemDataSyncForTypes(int, int)} (int, int)}. * Or by default all flags are 1 (enabled). */ public int getSystemDataSyncFlags() { return mSystemDataSyncFlags; } /** * Utility method for checking if the association represents a device with the given MAC * address. * * @return {@code false} if the association is "self-managed". * {@code false} if the {@code addr} is {@code null} or is not a valid MAC address. * Otherwise - the result of {@link MacAddress#equals(Object)} * * @hide */ public boolean isLinkedTo(@Nullable String addr) { if (mSelfManaged) return false; if (addr == null) return false; final MacAddress macAddress; try { macAddress = MacAddress.fromString(addr); } catch (IllegalArgumentException e) { return false; } return macAddress.equals(mDeviceMacAddress); } /** * Utility method to be used by CdmService only. * * @return whether CdmService should bind the companion application that "owns" this association * when the device is present. * * @hide */ public boolean shouldBindWhenPresent() { return mNotifyOnDeviceNearby || mSelfManaged; } /** @hide */ @NonNull public String toShortString() { final StringBuilder sb = new StringBuilder(); sb.append("id=").append(mId); if (mDeviceMacAddress != null) { sb.append(", addr=").append(getDeviceMacAddressAsString()); } if (mSelfManaged) { sb.append(", self-managed"); } sb.append(", pkg=u").append(mUserId).append('/').append(mPackageName); return sb.toString(); } @Override public String toString() { return "Association{" + "mId=" + mId + ", mUserId=" + mUserId + ", mPackageName='" + mPackageName + '\'' + ", mTag='" + mTag + '\'' + ", mDeviceMacAddress=" + mDeviceMacAddress + ", mDisplayName='" + mDisplayName + '\'' + ", mDeviceProfile='" + mDeviceProfile + '\'' + ", mSelfManaged=" + mSelfManaged + ", mAssociatedDevice=" + mAssociatedDevice + ", mNotifyOnDeviceNearby=" + mNotifyOnDeviceNearby + ", mRevoked=" + mRevoked + ", mPending=" + mPending + ", mTimeApprovedMs=" + new Date(mTimeApprovedMs) + ", mLastTimeConnectedMs=" + ( mLastTimeConnectedMs == Long.MAX_VALUE ? LAST_TIME_CONNECTED_NONE : new Date(mLastTimeConnectedMs)) + ", mSystemDataSyncFlags=" + mSystemDataSyncFlags + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof AssociationInfo)) return false; final AssociationInfo that = (AssociationInfo) o; return mId == that.mId && mUserId == that.mUserId && mSelfManaged == that.mSelfManaged && mNotifyOnDeviceNearby == that.mNotifyOnDeviceNearby && mRevoked == that.mRevoked && mPending == that.mPending && mTimeApprovedMs == that.mTimeApprovedMs && mLastTimeConnectedMs == that.mLastTimeConnectedMs && Objects.equals(mPackageName, that.mPackageName) && Objects.equals(mTag, that.mTag) && Objects.equals(mDeviceMacAddress, that.mDeviceMacAddress) && Objects.equals(mDisplayName, that.mDisplayName) && Objects.equals(mDeviceProfile, that.mDeviceProfile) && Objects.equals(mAssociatedDevice, that.mAssociatedDevice) && mSystemDataSyncFlags == that.mSystemDataSyncFlags; } @Override public int hashCode() { return Objects.hash(mId, mUserId, mPackageName, mTag, mDeviceMacAddress, mDisplayName, mDeviceProfile, mAssociatedDevice, mSelfManaged, mNotifyOnDeviceNearby, mRevoked, mPending, mTimeApprovedMs, mLastTimeConnectedMs, mSystemDataSyncFlags); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(mId); dest.writeInt(mUserId); dest.writeString(mPackageName); dest.writeString(mTag); dest.writeTypedObject(mDeviceMacAddress, 0); dest.writeCharSequence(mDisplayName); dest.writeString(mDeviceProfile); dest.writeTypedObject(mAssociatedDevice, 0); dest.writeBoolean(mSelfManaged); dest.writeBoolean(mNotifyOnDeviceNearby); dest.writeBoolean(mRevoked); dest.writeBoolean(mPending); dest.writeLong(mTimeApprovedMs); dest.writeLong(mLastTimeConnectedMs); dest.writeInt(mSystemDataSyncFlags); } private AssociationInfo(@NonNull Parcel in) { mId = in.readInt(); mUserId = in.readInt(); mPackageName = in.readString(); mTag = in.readString(); mDeviceMacAddress = in.readTypedObject(MacAddress.CREATOR); mDisplayName = in.readCharSequence(); mDeviceProfile = in.readString(); mAssociatedDevice = in.readTypedObject(AssociatedDevice.CREATOR); mSelfManaged = in.readBoolean(); mNotifyOnDeviceNearby = in.readBoolean(); mRevoked = in.readBoolean(); mPending = in.readBoolean(); mTimeApprovedMs = in.readLong(); mLastTimeConnectedMs = in.readLong(); mSystemDataSyncFlags = in.readInt(); } @NonNull public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override public AssociationInfo[] newArray(int size) { return new AssociationInfo[size]; } @Override public AssociationInfo createFromParcel(@NonNull Parcel in) { return new AssociationInfo(in); } }; /** * Builder for {@link AssociationInfo} * * @hide */ @FlaggedApi(Flags.FLAG_NEW_ASSOCIATION_BUILDER) @TestApi public static final class Builder { private final int mId; private final int mUserId; private final String mPackageName; private String mTag; private MacAddress mDeviceMacAddress; private CharSequence mDisplayName; private String mDeviceProfile; private AssociatedDevice mAssociatedDevice; private boolean mSelfManaged; private boolean mNotifyOnDeviceNearby; private boolean mRevoked; private boolean mPending; private long mTimeApprovedMs; private long mLastTimeConnectedMs; private int mSystemDataSyncFlags; /** @hide */ @TestApi public Builder(int id, int userId, @NonNull String packageName) { mId = id; mUserId = userId; mPackageName = packageName; } /** @hide */ @TestApi public Builder(@NonNull AssociationInfo info) { mId = info.mId; mUserId = info.mUserId; mPackageName = info.mPackageName; mTag = info.mTag; mDeviceMacAddress = info.mDeviceMacAddress; mDisplayName = info.mDisplayName; mDeviceProfile = info.mDeviceProfile; mAssociatedDevice = info.mAssociatedDevice; mSelfManaged = info.mSelfManaged; mNotifyOnDeviceNearby = info.mNotifyOnDeviceNearby; mRevoked = info.mRevoked; mPending = info.mPending; mTimeApprovedMs = info.mTimeApprovedMs; mLastTimeConnectedMs = info.mLastTimeConnectedMs; mSystemDataSyncFlags = info.mSystemDataSyncFlags; } /** * This builder is used specifically to create a new association to be restored to a device * that is potentially using a different user ID from the backed-up device. * * @hide */ public Builder(int id, int userId, @NonNull String packageName, AssociationInfo info) { mId = id; mUserId = userId; mPackageName = packageName; mTag = info.mTag; mDeviceMacAddress = info.mDeviceMacAddress; mDisplayName = info.mDisplayName; mDeviceProfile = info.mDeviceProfile; mAssociatedDevice = info.mAssociatedDevice; mSelfManaged = info.mSelfManaged; mNotifyOnDeviceNearby = info.mNotifyOnDeviceNearby; mRevoked = info.mRevoked; mPending = info.mPending; mTimeApprovedMs = info.mTimeApprovedMs; mLastTimeConnectedMs = info.mLastTimeConnectedMs; mSystemDataSyncFlags = info.mSystemDataSyncFlags; } /** @hide */ @FlaggedApi(Flags.FLAG_ASSOCIATION_TAG) @TestApi @NonNull public Builder setTag(@Nullable String tag) { mTag = tag; return this; } /** @hide */ @TestApi @NonNull public Builder setDeviceMacAddress(@Nullable MacAddress deviceMacAddress) { mDeviceMacAddress = deviceMacAddress; return this; } /** @hide */ @TestApi @NonNull public Builder setDisplayName(@Nullable CharSequence displayName) { mDisplayName = displayName; return this; } /** @hide */ @TestApi @NonNull public Builder setDeviceProfile(@Nullable String deviceProfile) { mDeviceProfile = deviceProfile; return this; } /** @hide */ @TestApi @NonNull public Builder setAssociatedDevice(@Nullable AssociatedDevice associatedDevice) { mAssociatedDevice = associatedDevice; return this; } /** @hide */ @TestApi @NonNull public Builder setSelfManaged(boolean selfManaged) { mSelfManaged = selfManaged; return this; } /** @hide */ @TestApi @NonNull @SuppressLint("MissingGetterMatchingBuilder") public Builder setNotifyOnDeviceNearby(boolean notifyOnDeviceNearby) { mNotifyOnDeviceNearby = notifyOnDeviceNearby; return this; } /** @hide */ @TestApi @NonNull @SuppressLint("MissingGetterMatchingBuilder") public Builder setRevoked(boolean revoked) { mRevoked = revoked; return this; } /** @hide */ @NonNull @SuppressLint("MissingGetterMatchingBuilder") public Builder setPending(boolean pending) { mPending = pending; return this; } /** @hide */ @TestApi @NonNull @SuppressLint("MissingGetterMatchingBuilder") public Builder setTimeApproved(long timeApprovedMs) { if (timeApprovedMs < 0) { throw new IllegalArgumentException("timeApprovedMs must be positive. Was given (" + timeApprovedMs + ")"); } mTimeApprovedMs = timeApprovedMs; return this; } /** @hide */ @TestApi @NonNull @SuppressLint("MissingGetterMatchingBuilder") public Builder setLastTimeConnected(long lastTimeConnectedMs) { if (lastTimeConnectedMs < 0) { throw new IllegalArgumentException( "lastTimeConnectedMs must not be negative! (Given " + lastTimeConnectedMs + " )"); } mLastTimeConnectedMs = lastTimeConnectedMs; return this; } /** @hide */ @TestApi @NonNull public Builder setSystemDataSyncFlags(int flags) { mSystemDataSyncFlags = flags; return this; } /** @hide */ @TestApi @NonNull public AssociationInfo build() { if (mId <= 0) { throw new IllegalArgumentException("Association ID should be greater than 0"); } if (mDeviceMacAddress == null && mDisplayName == null) { throw new IllegalArgumentException("MAC address and the display name must NOT be " + "null at the same time"); } return new AssociationInfo( mId, mUserId, mPackageName, mTag, mDeviceMacAddress, mDisplayName, mDeviceProfile, mAssociatedDevice, mSelfManaged, mNotifyOnDeviceNearby, mRevoked, mPending, mTimeApprovedMs, mLastTimeConnectedMs, mSystemDataSyncFlags ); } } }