diff options
268 files changed, 8353 insertions, 1662 deletions
diff --git a/apex/blobstore/framework/java/android/app/blob/BlobStoreManager.java b/apex/blobstore/framework/java/android/app/blob/BlobStoreManager.java index 483d2cc2ec57..9c1acafa800d 100644 --- a/apex/blobstore/framework/java/android/app/blob/BlobStoreManager.java +++ b/apex/blobstore/framework/java/android/app/blob/BlobStoreManager.java @@ -184,9 +184,8 @@ public class BlobStoreManager { * @throws SecurityException when the caller is not allowed to create a session, such * as when called from an Instant app. * @throws IllegalArgumentException when {@code blobHandle} is invalid. - * @throws IllegalStateException when a new session could not be created, such as when the - * caller is trying to create too many sessions or when the - * device is running low on space. + * @throws LimitExceededException when a new session could not be created, such as when the + * caller is trying to create too many sessions. */ public @IntRange(from = 1) long createSession(@NonNull BlobHandle blobHandle) throws IOException { @@ -194,6 +193,7 @@ public class BlobStoreManager { return mService.createSession(blobHandle, mContext.getOpPackageName()); } catch (ParcelableException e) { e.maybeRethrow(IOException.class); + e.maybeRethrow(LimitExceededException.class); throw new RuntimeException(e); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -302,8 +302,9 @@ public class BlobStoreManager { * if the {@code leaseExpiryTimeMillis} is greater than the * {@link BlobHandle#getExpiryTimeMillis()}. * @throws LimitExceededException when a lease could not be acquired, such as when the - * caller is trying to acquire leases on too much data. Apps - * can avoid this by checking the remaining quota using + * caller is trying to acquire too many leases or acquire + * leases on too much data. Apps can avoid this by checking + * the remaining quota using * {@link #getRemainingLeaseQuotaBytes()} before trying to * acquire a lease. * @@ -362,8 +363,9 @@ public class BlobStoreManager { * if the {@code leaseExpiryTimeMillis} is greater than the * {@link BlobHandle#getExpiryTimeMillis()}. * @throws LimitExceededException when a lease could not be acquired, such as when the - * caller is trying to acquire leases on too much data. Apps - * can avoid this by checking the remaining quota using + * caller is trying to acquire too many leases or acquire + * leases on too much data. Apps can avoid this by checking + * the remaining quota using * {@link #getRemainingLeaseQuotaBytes()} before trying to * acquire a lease. * @@ -415,8 +417,9 @@ public class BlobStoreManager { * exist or the caller does not have access to it. * @throws IllegalArgumentException when {@code blobHandle} is invalid. * @throws LimitExceededException when a lease could not be acquired, such as when the - * caller is trying to acquire leases on too much data. Apps - * can avoid this by checking the remaining quota using + * caller is trying to acquire too many leases or acquire + * leases on too much data. Apps can avoid this by checking + * the remaining quota using * {@link #getRemainingLeaseQuotaBytes()} before trying to * acquire a lease. * @@ -462,8 +465,9 @@ public class BlobStoreManager { * exist or the caller does not have access to it. * @throws IllegalArgumentException when {@code blobHandle} is invalid. * @throws LimitExceededException when a lease could not be acquired, such as when the - * caller is trying to acquire leases on too much data. Apps - * can avoid this by checking the remaining quota using + * caller is trying to acquire too many leases or acquire + * leases on too much data. Apps can avoid this by checking + * the remaining quota using * {@link #getRemainingLeaseQuotaBytes()} before trying to * acquire a lease. * @@ -757,6 +761,8 @@ public class BlobStoreManager { * @throws SecurityException when the caller is not the owner of the session. * @throws IllegalStateException when the caller tries to change access for a blob which is * already committed. + * @throws LimitExceededException when the caller tries to explicitly allow too + * many packages using this API. */ public void allowPackageAccess(@NonNull String packageName, @NonNull byte[] certificate) throws IOException { @@ -764,6 +770,7 @@ public class BlobStoreManager { mSession.allowPackageAccess(packageName, certificate); } catch (ParcelableException e) { e.maybeRethrow(IOException.class); + e.maybeRethrow(LimitExceededException.class); throw new RuntimeException(e); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java b/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java index 4d29045fa631..0b760a621d22 100644 --- a/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java +++ b/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java @@ -154,10 +154,10 @@ class BlobMetadata { } } - void removeInvalidCommitters(SparseArray<String> packages) { + void removeCommittersFromUnknownPkgs(SparseArray<String> knownPackages) { synchronized (mMetadataLock) { mCommitters.removeIf(committer -> - !committer.packageName.equals(packages.get(committer.uid))); + !committer.packageName.equals(knownPackages.get(committer.uid))); } } @@ -200,16 +200,27 @@ class BlobMetadata { } } - void removeInvalidLeasees(SparseArray<String> packages) { + void removeLeaseesFromUnknownPkgs(SparseArray<String> knownPackages) { synchronized (mMetadataLock) { mLeasees.removeIf(leasee -> - !leasee.packageName.equals(packages.get(leasee.uid))); + !leasee.packageName.equals(knownPackages.get(leasee.uid))); } } - boolean hasLeases() { + void removeExpiredLeases() { synchronized (mMetadataLock) { - return !mLeasees.isEmpty(); + mLeasees.removeIf(leasee -> !leasee.isStillValid()); + } + } + + boolean hasValidLeases() { + synchronized (mMetadataLock) { + for (int i = 0, size = mLeasees.size(); i < size; ++i) { + if (mLeasees.valueAt(i).isStillValid()) { + return true; + } + } + return false; } } @@ -226,8 +237,7 @@ class BlobMetadata { // Check if packageName already holds a lease on the blob. for (int i = 0, size = mLeasees.size(); i < size; ++i) { final Leasee leasee = mLeasees.valueAt(i); - if (leasee.equals(callingPackage, callingUid) - && leasee.isStillValid()) { + if (leasee.isStillValid() && leasee.equals(callingPackage, callingUid)) { return true; } } @@ -259,25 +269,32 @@ class BlobMetadata { boolean isALeasee(@Nullable String packageName, int uid) { synchronized (mMetadataLock) { - return isAnAccessor(mLeasees, packageName, uid); + final Leasee leasee = getAccessor(mLeasees, packageName, uid); + return leasee != null && leasee.isStillValid(); } } private static <T extends Accessor> boolean isAnAccessor(@NonNull ArraySet<T> accessors, @Nullable String packageName, int uid) { // Check if the package is an accessor of the data blob. + return getAccessor(accessors, packageName, uid) != null; + } + + private static <T extends Accessor> T getAccessor(@NonNull ArraySet<T> accessors, + @Nullable String packageName, int uid) { + // Check if the package is an accessor of the data blob. for (int i = 0, size = accessors.size(); i < size; ++i) { final Accessor accessor = accessors.valueAt(i); if (packageName != null && uid != INVALID_UID && accessor.equals(packageName, uid)) { - return true; + return (T) accessor; } else if (packageName != null && accessor.packageName.equals(packageName)) { - return true; + return (T) accessor; } else if (uid != INVALID_UID && accessor.uid == uid) { - return true; + return (T) accessor; } } - return false; + return null; } boolean isALeasee(@NonNull String packageName) { @@ -298,11 +315,11 @@ class BlobMetadata { private boolean hasOtherLeasees(@Nullable String packageName, int uid) { synchronized (mMetadataLock) { - if (mCommitters.size() > 1 || mLeasees.size() > 1) { - return true; - } for (int i = 0, size = mLeasees.size(); i < size; ++i) { final Leasee leasee = mLeasees.valueAt(i); + if (!leasee.isStillValid()) { + continue; + } // TODO: Also exclude packages which are signed with same cert? if (packageName != null && uid != INVALID_UID && !leasee.equals(packageName, uid)) { @@ -322,6 +339,9 @@ class BlobMetadata { synchronized (mMetadataLock) { for (int i = 0, size = mLeasees.size(); i < size; ++i) { final Leasee leasee = mLeasees.valueAt(i); + if (!leasee.isStillValid()) { + continue; + } if (leasee.uid == uid && leasee.packageName.equals(packageName)) { final int descriptionResId = leasee.descriptionResEntryName == null ? Resources.ID_NULL @@ -398,6 +418,26 @@ class BlobMetadata { return revocableFd.getRevocableFileDescriptor(); } + void destroy() { + revokeAllFds(); + getBlobFile().delete(); + } + + private void revokeAllFds() { + synchronized (mRevocableFds) { + for (int i = 0, pkgCount = mRevocableFds.size(); i < pkgCount; ++i) { + final ArraySet<RevocableFileDescriptor> packageFds = + mRevocableFds.valueAt(i); + if (packageFds == null) { + continue; + } + for (int j = 0, fdCount = packageFds.size(); j < fdCount; ++j) { + packageFds.valueAt(j).revoke(); + } + } + } + } + boolean shouldBeDeleted(boolean respectLeaseWaitTime) { // Expired data blobs if (getBlobHandle().isExpired()) { @@ -406,7 +446,7 @@ class BlobMetadata { // Blobs with no active leases if ((!respectLeaseWaitTime || hasLeaseWaitTimeElapsedForAll()) - && !hasLeases()) { + && !hasValidLeases()) { return true; } @@ -695,7 +735,7 @@ class BlobMetadata { } boolean isStillValid() { - return expiryTimeMillis == 0 || expiryTimeMillis <= System.currentTimeMillis(); + return expiryTimeMillis == 0 || expiryTimeMillis >= System.currentTimeMillis(); } void dump(@NonNull Context context, @NonNull IndentingPrintWriter fout) { diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java index 265479f7c533..79cd1b17a5b5 100644 --- a/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java +++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java @@ -141,6 +141,36 @@ class BlobStoreConfig { public static long DELETE_ON_LAST_LEASE_DELAY_MS = DEFAULT_DELETE_ON_LAST_LEASE_DELAY_MS; + /** + * Denotes the maximum number of active sessions per app at any time. + */ + public static final String KEY_MAX_ACTIVE_SESSIONS = "max_active_sessions"; + public static int DEFAULT_MAX_ACTIVE_SESSIONS = 250; + public static int MAX_ACTIVE_SESSIONS = DEFAULT_MAX_ACTIVE_SESSIONS; + + /** + * Denotes the maximum number of committed blobs per app at any time. + */ + public static final String KEY_MAX_COMMITTED_BLOBS = "max_committed_blobs"; + public static int DEFAULT_MAX_COMMITTED_BLOBS = 1000; + public static int MAX_COMMITTED_BLOBS = DEFAULT_MAX_COMMITTED_BLOBS; + + /** + * Denotes the maximum number of leased blobs per app at any time. + */ + public static final String KEY_MAX_LEASED_BLOBS = "max_leased_blobs"; + public static int DEFAULT_MAX_LEASED_BLOBS = 500; + public static int MAX_LEASED_BLOBS = DEFAULT_MAX_LEASED_BLOBS; + + /** + * Denotes the maximum number of packages explicitly permitted to access a blob + * (permitted as part of creating a {@link BlobAccessMode}). + */ + public static final String KEY_MAX_BLOB_ACCESS_PERMITTED_PACKAGES = "max_permitted_pks"; + public static int DEFAULT_MAX_BLOB_ACCESS_PERMITTED_PACKAGES = 300; + public static int MAX_BLOB_ACCESS_PERMITTED_PACKAGES = + DEFAULT_MAX_BLOB_ACCESS_PERMITTED_PACKAGES; + static void refresh(Properties properties) { if (!NAMESPACE_BLOBSTORE.equals(properties.getNamespace())) { return; @@ -178,6 +208,19 @@ class BlobStoreConfig { DELETE_ON_LAST_LEASE_DELAY_MS = properties.getLong(key, DEFAULT_DELETE_ON_LAST_LEASE_DELAY_MS); break; + case KEY_MAX_ACTIVE_SESSIONS: + MAX_ACTIVE_SESSIONS = properties.getInt(key, DEFAULT_MAX_ACTIVE_SESSIONS); + break; + case KEY_MAX_COMMITTED_BLOBS: + MAX_COMMITTED_BLOBS = properties.getInt(key, DEFAULT_MAX_COMMITTED_BLOBS); + break; + case KEY_MAX_LEASED_BLOBS: + MAX_LEASED_BLOBS = properties.getInt(key, DEFAULT_MAX_LEASED_BLOBS); + break; + case KEY_MAX_BLOB_ACCESS_PERMITTED_PACKAGES: + MAX_BLOB_ACCESS_PERMITTED_PACKAGES = properties.getInt(key, + DEFAULT_MAX_BLOB_ACCESS_PERMITTED_PACKAGES); + break; default: Slog.wtf(TAG, "Unknown key in device config properties: " + key); } @@ -210,6 +253,15 @@ class BlobStoreConfig { fout.println(String.format(dumpFormat, KEY_DELETE_ON_LAST_LEASE_DELAY_MS, TimeUtils.formatDuration(DELETE_ON_LAST_LEASE_DELAY_MS), TimeUtils.formatDuration(DEFAULT_DELETE_ON_LAST_LEASE_DELAY_MS))); + fout.println(String.format(dumpFormat, KEY_MAX_ACTIVE_SESSIONS, + MAX_ACTIVE_SESSIONS, DEFAULT_MAX_ACTIVE_SESSIONS)); + fout.println(String.format(dumpFormat, KEY_MAX_COMMITTED_BLOBS, + MAX_COMMITTED_BLOBS, DEFAULT_MAX_COMMITTED_BLOBS)); + fout.println(String.format(dumpFormat, KEY_MAX_LEASED_BLOBS, + MAX_LEASED_BLOBS, DEFAULT_MAX_LEASED_BLOBS)); + fout.println(String.format(dumpFormat, KEY_MAX_BLOB_ACCESS_PERMITTED_PACKAGES, + MAX_BLOB_ACCESS_PERMITTED_PACKAGES, + DEFAULT_MAX_BLOB_ACCESS_PERMITTED_PACKAGES)); } } @@ -288,6 +340,34 @@ class BlobStoreConfig { return DeviceConfigProperties.DELETE_ON_LAST_LEASE_DELAY_MS; } + /** + * Returns the maximum number of active sessions per app. + */ + public static int getMaxActiveSessions() { + return DeviceConfigProperties.MAX_ACTIVE_SESSIONS; + } + + /** + * Returns the maximum number of committed blobs per app. + */ + public static int getMaxCommittedBlobs() { + return DeviceConfigProperties.MAX_COMMITTED_BLOBS; + } + + /** + * Returns the maximum number of leased blobs per app. + */ + public static int getMaxLeasedBlobs() { + return DeviceConfigProperties.MAX_LEASED_BLOBS; + } + + /** + * Returns the maximum number of packages explicitly permitted to access a blob. + */ + public static int getMaxPermittedPackages() { + return DeviceConfigProperties.MAX_BLOB_ACCESS_PERMITTED_PACKAGES; + } + @Nullable public static File prepareBlobFile(long sessionId) { final File blobsDir = prepareBlobsDir(); diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java index a90536fee904..d9c0e478f1e3 100644 --- a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java +++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java @@ -35,6 +35,9 @@ import static com.android.server.blob.BlobStoreConfig.TAG; import static com.android.server.blob.BlobStoreConfig.XML_VERSION_CURRENT; import static com.android.server.blob.BlobStoreConfig.getAdjustedCommitTimeMs; import static com.android.server.blob.BlobStoreConfig.getDeletionOnLastLeaseDelayMs; +import static com.android.server.blob.BlobStoreConfig.getMaxActiveSessions; +import static com.android.server.blob.BlobStoreConfig.getMaxCommittedBlobs; +import static com.android.server.blob.BlobStoreConfig.getMaxLeasedBlobs; import static com.android.server.blob.BlobStoreSession.STATE_ABANDONED; import static com.android.server.blob.BlobStoreSession.STATE_COMMITTED; import static com.android.server.blob.BlobStoreSession.STATE_VERIFIED_INVALID; @@ -124,6 +127,7 @@ import java.util.List; import java.util.Objects; import java.util.Random; import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; import java.util.function.Function; @@ -332,9 +336,26 @@ public class BlobStoreManagerService extends SystemService { mKnownBlobIds.add(id); } + @GuardedBy("mBlobsLock") + private int getSessionsCountLocked(int uid, String packageName) { + // TODO: Maintain a counter instead of traversing all the sessions + final AtomicInteger sessionsCount = new AtomicInteger(0); + forEachSessionInUser(session -> { + if (session.getOwnerUid() == uid && session.getOwnerPackageName().equals(packageName)) { + sessionsCount.getAndIncrement(); + } + }, UserHandle.getUserId(uid)); + return sessionsCount.get(); + } + private long createSessionInternal(BlobHandle blobHandle, int callingUid, String callingPackage) { synchronized (mBlobsLock) { + final int sessionsCount = getSessionsCountLocked(callingUid, callingPackage); + if (sessionsCount >= getMaxActiveSessions()) { + throw new LimitExceededException("Too many active sessions for the caller: " + + sessionsCount); + } // TODO: throw if there is already an active session associated with blobHandle. final long sessionId = generateNextSessionIdLocked(); final BlobStoreSession session = new BlobStoreSession(mContext, @@ -408,10 +429,39 @@ public class BlobStoreManagerService extends SystemService { } } + @GuardedBy("mBlobsLock") + private int getCommittedBlobsCountLocked(int uid, String packageName) { + // TODO: Maintain a counter instead of traversing all the blobs + final AtomicInteger blobsCount = new AtomicInteger(0); + forEachBlobInUser((blobMetadata) -> { + if (blobMetadata.isACommitter(packageName, uid)) { + blobsCount.getAndIncrement(); + } + }, UserHandle.getUserId(uid)); + return blobsCount.get(); + } + + @GuardedBy("mBlobsLock") + private int getLeasedBlobsCountLocked(int uid, String packageName) { + // TODO: Maintain a counter instead of traversing all the blobs + final AtomicInteger blobsCount = new AtomicInteger(0); + forEachBlobInUser((blobMetadata) -> { + if (blobMetadata.isALeasee(packageName, uid)) { + blobsCount.getAndIncrement(); + } + }, UserHandle.getUserId(uid)); + return blobsCount.get(); + } + private void acquireLeaseInternal(BlobHandle blobHandle, int descriptionResId, CharSequence description, long leaseExpiryTimeMillis, int callingUid, String callingPackage) { synchronized (mBlobsLock) { + final int leasesCount = getLeasedBlobsCountLocked(callingUid, callingPackage); + if (leasesCount >= getMaxLeasedBlobs()) { + throw new LimitExceededException("Too many leased blobs for the caller: " + + leasesCount); + } final BlobMetadata blobMetadata = getUserBlobsLocked(UserHandle.getUserId(callingUid)) .get(blobHandle); if (blobMetadata == null || !blobMetadata.isAccessAllowedForCaller( @@ -489,7 +539,7 @@ public class BlobStoreManagerService extends SystemService { Slog.v(TAG, "Released lease on " + blobHandle + "; callingUid=" + callingUid + ", callingPackage=" + callingPackage); } - if (!blobMetadata.hasLeases()) { + if (!blobMetadata.hasValidLeases()) { mHandler.postDelayed(() -> { synchronized (mBlobsLock) { // Check if blobMetadata object is still valid. If it is not, then @@ -533,6 +583,9 @@ public class BlobStoreManagerService extends SystemService { getUserBlobsLocked(userId).forEach((blobHandle, blobMetadata) -> { final ArrayList<LeaseInfo> leaseInfos = new ArrayList<>(); blobMetadata.forEachLeasee(leasee -> { + if (!leasee.isStillValid()) { + return; + } final int descriptionResId = leasee.descriptionResEntryName == null ? Resources.ID_NULL : getDescriptionResourceId(resourcesGetter.apply(leasee.packageName), @@ -556,7 +609,11 @@ public class BlobStoreManagerService extends SystemService { UserHandle.getUserId(callingUid)); userBlobs.entrySet().removeIf(entry -> { final BlobMetadata blobMetadata = entry.getValue(); - return blobMetadata.getBlobId() == blobId; + if (blobMetadata.getBlobId() == blobId) { + deleteBlobLocked(blobMetadata); + return true; + } + return false; }); writeBlobsInfoAsync(); } @@ -607,11 +664,10 @@ public class BlobStoreManagerService extends SystemService { switch (session.getState()) { case STATE_ABANDONED: case STATE_VERIFIED_INVALID: - session.getSessionFile().delete(); synchronized (mBlobsLock) { + deleteSessionLocked(session); getUserSessionsLocked(UserHandle.getUserId(session.getOwnerUid())) .remove(session.getSessionId()); - mActiveBlobIds.remove(session.getSessionId()); if (LOGV) { Slog.v(TAG, "Session is invalid; deleted " + session); } @@ -626,6 +682,17 @@ public class BlobStoreManagerService extends SystemService { break; case STATE_VERIFIED_VALID: synchronized (mBlobsLock) { + final int committedBlobsCount = getCommittedBlobsCountLocked( + session.getOwnerUid(), session.getOwnerPackageName()); + if (committedBlobsCount >= getMaxCommittedBlobs()) { + Slog.d(TAG, "Failed to commit: too many committed blobs. count: " + + committedBlobsCount + "; blob: " + session); + session.sendCommitCallbackResult(COMMIT_RESULT_ERROR); + deleteSessionLocked(session); + getUserSessionsLocked(UserHandle.getUserId(session.getOwnerUid())) + .remove(session.getSessionId()); + break; + } final int userId = UserHandle.getUserId(session.getOwnerUid()); final ArrayMap<BlobHandle, BlobMetadata> userBlobs = getUserBlobsLocked( userId); @@ -656,7 +723,7 @@ public class BlobStoreManagerService extends SystemService { } else { blob.addOrReplaceCommitter(existingCommitter); } - Slog.d(TAG, "Error committing the blob", e); + Slog.d(TAG, "Error committing the blob: " + session, e); FrameworkStatsLog.write(FrameworkStatsLog.BLOB_COMMITTED, session.getOwnerUid(), blob.getBlobId(), blob.getSize(), FrameworkStatsLog.BLOB_COMMITTED__RESULT__ERROR_DURING_COMMIT); @@ -670,8 +737,7 @@ public class BlobStoreManagerService extends SystemService { } // Delete redundant data from recommits. if (session.getSessionId() != blob.getBlobId()) { - session.getSessionFile().delete(); - mActiveBlobIds.remove(session.getSessionId()); + deleteSessionLocked(session); } getUserSessionsLocked(UserHandle.getUserId(session.getOwnerUid())) .remove(session.getSessionId()); @@ -859,8 +925,8 @@ public class BlobStoreManagerService extends SystemService { blobMetadata.getBlobFile().delete(); } else { addBlobForUserLocked(blobMetadata, blobMetadata.getUserId()); - blobMetadata.removeInvalidCommitters(userPackages); - blobMetadata.removeInvalidLeasees(userPackages); + blobMetadata.removeCommittersFromUnknownPkgs(userPackages); + blobMetadata.removeLeaseesFromUnknownPkgs(userPackages); } mCurrentMaxSessionId = Math.max(mCurrentMaxSessionId, blobMetadata.getBlobId()); } @@ -957,8 +1023,7 @@ public class BlobStoreManagerService extends SystemService { userSessions.removeIf((sessionId, blobStoreSession) -> { if (blobStoreSession.getOwnerUid() == uid && blobStoreSession.getOwnerPackageName().equals(packageName)) { - blobStoreSession.getSessionFile().delete(); - mActiveBlobIds.remove(blobStoreSession.getSessionId()); + deleteSessionLocked(blobStoreSession); return true; } return false; @@ -999,8 +1064,7 @@ public class BlobStoreManagerService extends SystemService { if (userSessions != null) { for (int i = 0, count = userSessions.size(); i < count; ++i) { final BlobStoreSession session = userSessions.valueAt(i); - session.getSessionFile().delete(); - mActiveBlobIds.remove(session.getSessionId()); + deleteSessionLocked(session); } } @@ -1049,6 +1113,9 @@ public class BlobStoreManagerService extends SystemService { userBlobs.entrySet().removeIf(entry -> { final BlobMetadata blobMetadata = entry.getValue(); + // Remove expired leases + blobMetadata.removeExpiredLeases(); + if (blobMetadata.shouldBeDeleted(true /* respectLeaseWaitTime */)) { deleteBlobLocked(blobMetadata); deletedBlobIds.add(blobMetadata.getBlobId()); @@ -1076,8 +1143,7 @@ public class BlobStoreManagerService extends SystemService { } if (shouldRemove) { - blobStoreSession.getSessionFile().delete(); - mActiveBlobIds.remove(blobStoreSession.getSessionId()); + deleteSessionLocked(blobStoreSession); deletedBlobIds.add(blobStoreSession.getSessionId()); } return shouldRemove; @@ -1089,13 +1155,29 @@ public class BlobStoreManagerService extends SystemService { } @GuardedBy("mBlobsLock") + private void deleteSessionLocked(BlobStoreSession blobStoreSession) { + blobStoreSession.destroy(); + mActiveBlobIds.remove(blobStoreSession.getSessionId()); + } + + @GuardedBy("mBlobsLock") private void deleteBlobLocked(BlobMetadata blobMetadata) { - blobMetadata.getBlobFile().delete(); + blobMetadata.destroy(); mActiveBlobIds.remove(blobMetadata.getBlobId()); } void runClearAllSessions(@UserIdInt int userId) { synchronized (mBlobsLock) { + for (int i = 0, userCount = mSessions.size(); i < userCount; ++i) { + final int sessionUserId = mSessions.keyAt(i); + if (userId != UserHandle.USER_ALL && userId != sessionUserId) { + continue; + } + final LongSparseArray<BlobStoreSession> userSessions = mSessions.valueAt(i); + for (int j = 0, sessionsCount = userSessions.size(); j < sessionsCount; ++j) { + mActiveBlobIds.remove(userSessions.valueAt(j).getSessionId()); + } + } if (userId == UserHandle.USER_ALL) { mSessions.clear(); } else { @@ -1107,6 +1189,16 @@ public class BlobStoreManagerService extends SystemService { void runClearAllBlobs(@UserIdInt int userId) { synchronized (mBlobsLock) { + for (int i = 0, userCount = mBlobsMap.size(); i < userCount; ++i) { + final int blobUserId = mBlobsMap.keyAt(i); + if (userId != UserHandle.USER_ALL && userId != blobUserId) { + continue; + } + final ArrayMap<BlobHandle, BlobMetadata> userBlobs = mBlobsMap.valueAt(i); + for (int j = 0, blobsCount = userBlobs.size(); j < blobsCount; ++j) { + mActiveBlobIds.remove(userBlobs.valueAt(j).getBlobId()); + } + } if (userId == UserHandle.USER_ALL) { mBlobsMap.clear(); } else { @@ -1331,8 +1423,11 @@ public class BlobStoreManagerService extends SystemService { + "callingUid=" + callingUid + ", callingPackage=" + packageName); } - // TODO: Verify caller request is within limits (no. of calls/blob sessions/blobs) - return createSessionInternal(blobHandle, callingUid, packageName); + try { + return createSessionInternal(blobHandle, callingUid, packageName); + } catch (LimitExceededException e) { + throw new ParcelableException(e); + } } @Override diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java index baafff53d072..2f83be1e0370 100644 --- a/apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java +++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java @@ -32,6 +32,7 @@ import static android.text.format.Formatter.formatFileSize; import static com.android.server.blob.BlobStoreConfig.TAG; import static com.android.server.blob.BlobStoreConfig.XML_VERSION_ADD_SESSION_CREATION_TIME; +import static com.android.server.blob.BlobStoreConfig.getMaxPermittedPackages; import static com.android.server.blob.BlobStoreConfig.hasSessionExpired; import android.annotation.BytesLong; @@ -43,7 +44,9 @@ import android.app.blob.IBlobStoreSession; import android.content.Context; import android.os.Binder; import android.os.FileUtils; +import android.os.LimitExceededException; import android.os.ParcelFileDescriptor; +import android.os.ParcelableException; import android.os.RemoteException; import android.os.RevocableFileDescriptor; import android.os.Trace; @@ -76,7 +79,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Objects; -/** TODO: add doc */ +/** + * Class to represent the state corresponding to an ongoing + * {@link android.app.blob.BlobStoreManager.Session} + */ @VisibleForTesting class BlobStoreSession extends IBlobStoreSession.Stub { @@ -326,6 +332,11 @@ class BlobStoreSession extends IBlobStoreSession.Stub { throw new IllegalStateException("Not allowed to change access type in state: " + stateToString(mState)); } + if (mBlobAccessMode.getNumWhitelistedPackages() >= getMaxPermittedPackages()) { + throw new ParcelableException(new LimitExceededException( + "Too many packages permitted to access the blob: " + + mBlobAccessMode.getNumWhitelistedPackages())); + } mBlobAccessMode.allowPackageAccess(packageName, certificate); } } @@ -468,6 +479,11 @@ class BlobStoreSession extends IBlobStoreSession.Stub { } } + void destroy() { + revokeAllFds(); + getSessionFile().delete(); + } + private void revokeAllFds() { synchronized (mRevocableFds) { for (int i = mRevocableFds.size() - 1; i >= 0; --i) { diff --git a/apex/media/framework/java/android/media/MediaParser.java b/apex/media/framework/java/android/media/MediaParser.java index 8a3fbde5609b..0c8c9a9c63e0 100644 --- a/apex/media/framework/java/android/media/MediaParser.java +++ b/apex/media/framework/java/android/media/MediaParser.java @@ -841,6 +841,43 @@ public final class MediaParser { */ public static final String PARAMETER_EXPOSE_CHUNK_INDEX_AS_MEDIA_FORMAT = "android.media.mediaParser.exposeChunkIndexAsMediaFormat"; + /** + * Sets a list of closed-caption {@link MediaFormat MediaFormats} that should be exposed as part + * of the extracted media. {@code List<MediaFormat>} expected. Default value is an empty list. + * + * <p>Expected keys in the {@link MediaFormat} are: + * + * <ul> + * <p>{@link MediaFormat#KEY_MIME}: Determine the type of captions (for example, + * application/cea-608). Mandatory. + * <p>{@link MediaFormat#KEY_CAPTION_SERVICE_NUMBER}: Determine the channel on which the + * captions are transmitted. Optional. + * </ul> + * + * @hide + */ + public static final String PARAMETER_EXPOSE_CAPTION_FORMATS = + "android.media.mediaParser.exposeCaptionFormats"; + /** + * Sets whether the value associated with {@link #PARAMETER_EXPOSE_CAPTION_FORMATS} should + * override any in-band caption service declarations. {@code boolean} expected. Default value is + * {@link false}. + * + * <p>When {@code false}, any present in-band caption services information will override the + * values associated with {@link #PARAMETER_EXPOSE_CAPTION_FORMATS}. + * + * @hide + */ + public static final String PARAMETER_OVERRIDE_IN_BAND_CAPTION_DECLARATIONS = + "android.media.mediaParser.overrideInBandCaptionDeclarations"; + /** + * Sets whether a track for EMSG events should be exposed in case of parsing a container that + * supports them. {@code boolean} expected. Default value is {@link false}. + * + * @hide + */ + public static final String PARAMETER_EXPOSE_EMSG_TRACK = + "android.media.mediaParser.exposeEmsgTrack"; // Private constants. @@ -851,6 +888,7 @@ public final class MediaParser { private static final String TS_MODE_MULTI_PMT = "multi_pmt"; private static final String TS_MODE_HLS = "hls"; private static final int BYTES_PER_SUBSAMPLE_ENCRYPTION_ENTRY = 6; + private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; @IntDef( value = { @@ -1000,6 +1038,7 @@ public final class MediaParser { private final DataReaderAdapter mScratchDataReaderAdapter; private final ParsableByteArrayAdapter mScratchParsableByteArrayAdapter; @Nullable private final Constructor<DrmInitData.SchemeInitData> mSchemeInitDataConstructor; + private final ArrayList<Format> mMuxedCaptionFormats; private boolean mInBandCryptoInfo; private boolean mIncludeSupplementalData; private boolean mIgnoreTimestampOffset; @@ -1071,6 +1110,9 @@ public final class MediaParser { if (PARAMETER_EXPOSE_CHUNK_INDEX_AS_MEDIA_FORMAT.equals(parameterName)) { mExposeChunkIndexAsMediaFormat = (boolean) value; } + if (PARAMETER_EXPOSE_CAPTION_FORMATS.equals(parameterName)) { + setMuxedCaptionFormats((List<MediaFormat>) value); + } mParserParameters.put(parameterName, value); return this; } @@ -1109,8 +1151,8 @@ public final class MediaParser { * * <p>This method will block until some progress has been made. * - * <p>If this instance was created using {@link #create}. the first call to this method will - * sniff the content with the parsers with the provided names. + * <p>If this instance was created using {@link #create}, the first call to this method will + * sniff the content using the selected parser implementations. * * @param seekableInputReader The {@link SeekableInputReader} from which to obtain the media * container data. @@ -1242,6 +1284,14 @@ public final class MediaParser { mScratchDataReaderAdapter = new DataReaderAdapter(); mScratchParsableByteArrayAdapter = new ParsableByteArrayAdapter(); mSchemeInitDataConstructor = getSchemeInitDataConstructor(); + mMuxedCaptionFormats = new ArrayList<>(); + } + + private void setMuxedCaptionFormats(List<MediaFormat> mediaFormats) { + mMuxedCaptionFormats.clear(); + for (MediaFormat mediaFormat : mediaFormats) { + mMuxedCaptionFormats.add(toExoPlayerCaptionFormat(mediaFormat)); + } } private boolean isPendingSeek() { @@ -1268,6 +1318,10 @@ public final class MediaParser { return new MatroskaExtractor(flags); case PARSER_NAME_FMP4: flags |= + getBooleanParameter(PARAMETER_EXPOSE_EMSG_TRACK) + ? FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK + : 0; + flags |= getBooleanParameter(PARAMETER_MP4_IGNORE_EDIT_LISTS) ? FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_EDIT_LISTS : 0; @@ -1280,7 +1334,11 @@ public final class MediaParser { ? FragmentedMp4Extractor .FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME : 0; - return new FragmentedMp4Extractor(flags, timestampAdjuster); + return new FragmentedMp4Extractor( + flags, + timestampAdjuster, + /* sideloadedTrack= */ null, + mMuxedCaptionFormats); case PARSER_NAME_MP4: flags |= getBooleanParameter(PARAMETER_MP4_IGNORE_EDIT_LISTS) @@ -1331,6 +1389,10 @@ public final class MediaParser { getBooleanParameter(PARAMETER_TS_IGNORE_SPLICE_INFO_STREAM) ? DefaultTsPayloadReaderFactory.FLAG_IGNORE_SPLICE_INFO_STREAM : 0; + flags |= + getBooleanParameter(PARAMETER_OVERRIDE_IN_BAND_CAPTION_DECLARATIONS) + ? DefaultTsPayloadReaderFactory.FLAG_OVERRIDE_CAPTION_DESCRIPTORS + : 0; String tsMode = getStringParameter(PARAMETER_TS_MODE, TS_MODE_SINGLE_PMT); int hlsMode = TS_MODE_SINGLE_PMT.equals(tsMode) @@ -1343,7 +1405,7 @@ public final class MediaParser { timestampAdjuster != null ? timestampAdjuster : new TimestampAdjuster(/* firstSampleTimestampUs= */ 0), - new DefaultTsPayloadReaderFactory(flags)); + new DefaultTsPayloadReaderFactory(flags, mMuxedCaptionFormats)); case PARSER_NAME_FLV: return new FlvExtractor(); case PARSER_NAME_OGG: @@ -1625,6 +1687,9 @@ public final class MediaParser { if (cryptoData != mLastReceivedCryptoData) { mLastOutputCryptoInfo = createNewCryptoInfoAndPopulateWithCryptoData(cryptoData); + // We are using in-band crypto info, so the IV will be ignored. But we prevent + // it from being null because toString assumes it non-null. + mLastOutputCryptoInfo.iv = EMPTY_BYTE_ARRAY; } } else /* We must populate the full CryptoInfo. */ { // CryptoInfo.pattern is not accessible to the user, so the user needs to feed @@ -1789,6 +1854,16 @@ public final class MediaParser { // Private static methods. + private static Format toExoPlayerCaptionFormat(MediaFormat mediaFormat) { + Format.Builder formatBuilder = + new Format.Builder().setSampleMimeType(mediaFormat.getString(MediaFormat.KEY_MIME)); + if (mediaFormat.containsKey(MediaFormat.KEY_CAPTION_SERVICE_NUMBER)) { + formatBuilder.setAccessibilityChannel( + mediaFormat.getInteger(MediaFormat.KEY_CAPTION_SERVICE_NUMBER)); + } + return formatBuilder.build(); + } + private static MediaFormat toMediaFormat(Format format) { MediaFormat result = new MediaFormat(); setOptionalMediaFormatInt(result, MediaFormat.KEY_BIT_RATE, format.bitrate); @@ -1857,8 +1932,10 @@ public final class MediaParser { // format for convenient use from ExoPlayer. result.setString("crypto-mode-fourcc", format.drmInitData.schemeType); } + if (format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) { + result.setLong("subsample-offset-us-long", format.subsampleOffsetUs); + } // LACK OF SUPPORT FOR: - // format.containerMimeType; // format.id; // format.metadata; // format.stereoMode; @@ -2041,6 +2118,12 @@ public final class MediaParser { expectedTypeByParameterName.put(PARAMETER_EXPOSE_DUMMY_SEEKMAP, Boolean.class); expectedTypeByParameterName.put( PARAMETER_EXPOSE_CHUNK_INDEX_AS_MEDIA_FORMAT, Boolean.class); + expectedTypeByParameterName.put( + PARAMETER_OVERRIDE_IN_BAND_CAPTION_DECLARATIONS, Boolean.class); + expectedTypeByParameterName.put(PARAMETER_EXPOSE_EMSG_TRACK, Boolean.class); + // We do not check PARAMETER_EXPOSE_CAPTION_FORMATS here, and we do it in setParameters + // instead. Checking that the value is a List is insufficient to catch wrong parameter + // value types. EXPECTED_TYPE_BY_PARAMETER_NAME = Collections.unmodifiableMap(expectedTypeByParameterName); } } diff --git a/apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/libstats/LibStatsPullTests.java b/apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/libstats/LibStatsPullTests.java index 240222ea9411..6108a324e15e 100644 --- a/apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/libstats/LibStatsPullTests.java +++ b/apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/libstats/LibStatsPullTests.java @@ -33,7 +33,6 @@ import com.android.internal.os.StatsdConfigProto.PullAtomPackages; import com.android.internal.os.StatsdConfigProto.SimpleAtomMatcher; import com.android.internal.os.StatsdConfigProto.StatsdConfig; import com.android.internal.os.StatsdConfigProto.TimeUnit; -import com.android.internal.os.statsd.StatsConfigUtils; import com.android.internal.os.statsd.protos.TestAtoms; import com.android.os.AtomsProto.Atom; diff --git a/apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/StatsConfigUtils.java b/apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/libstats/StatsConfigUtils.java index d0d140092586..b5afb94886de 100644 --- a/apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/StatsConfigUtils.java +++ b/apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/libstats/StatsConfigUtils.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.internal.os.statsd; +package com.android.internal.os.statsd.libstats; import static com.google.common.truth.Truth.assertThat; diff --git a/api/test-current.txt b/api/test-current.txt index 5bae37041edd..7fd56984799f 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -5264,6 +5264,7 @@ package android.view.autofill { ctor public AutofillId(int, int); ctor public AutofillId(@NonNull android.view.autofill.AutofillId, long, int); method public boolean equalsIgnoreSession(@Nullable android.view.autofill.AutofillId); + method @NonNull public static android.view.autofill.AutofillId withoutSession(@NonNull android.view.autofill.AutofillId); } public final class AutofillManager { @@ -5505,6 +5506,19 @@ package android.widget { } +package android.widget.inline { + + public class InlineContentView extends android.view.ViewGroup { + method public void setChildSurfacePackageUpdater(@Nullable android.widget.inline.InlineContentView.SurfacePackageUpdater); + } + + public static interface InlineContentView.SurfacePackageUpdater { + method public void getSurfacePackage(@NonNull java.util.function.Consumer<android.view.SurfaceControlViewHost.SurfacePackage>); + method public void onSurfacePackageReleased(); + } + +} + package android.window { public final class DisplayAreaInfo implements android.os.Parcelable { diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto index a5f0ac97cebc..02c0763c9d83 100644 --- a/cmds/statsd/src/atoms.proto +++ b/cmds/statsd/src/atoms.proto @@ -6131,6 +6131,10 @@ message ProcessStatsAvailablePagesProto { */ message ProcStats { optional ProcessStatsSectionProto proc_stats_section = 1; + // Data pulled from device into this is sometimes sharded across multiple atoms to work around + // a size limit. When this happens, this shard ID will contain an increasing 1-indexed integer + // with the number of this shard. + optional int32 shard_id = 2; } /** @@ -9690,6 +9694,7 @@ message RuntimeAppOpAccess { UNIFORM = 1; RARELY_USED = 2; BOOT_TIME_SAMPLING = 3; + UNIFORM_OPS = 4; } // sampling strategy used to collect this message diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index 6f8233d5de9b..c9031b711657 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -763,26 +763,27 @@ public class ApplicationPackageManager extends PackageManager { @Override public void revokeRuntimePermission(String packageName, String permName, UserHandle user) { + revokeRuntimePermission(packageName, permName, user, null); + } + + @Override + public void revokeRuntimePermission(String packageName, String permName, UserHandle user, + String reason) { if (DEBUG_TRACE_PERMISSION_UPDATES && shouldTraceGrant(packageName, permName, user.getIdentifier())) { Log.i(TAG, "App " + mContext.getPackageName() + " is revoking " + packageName + " " - + permName + " for user " + user.getIdentifier(), new RuntimeException()); + + permName + " for user " + user.getIdentifier() + " with reason " + reason, + new RuntimeException()); } try { mPermissionManager - .revokeRuntimePermission(packageName, permName, user.getIdentifier()); + .revokeRuntimePermission(packageName, permName, user.getIdentifier(), reason); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } @Override - public void revokeRuntimePermission(String packageName, String permName, UserHandle user, - String reason) { - // TODO evanseverson: impl - } - - @Override public int getPermissionFlags(String permName, String packageName, UserHandle user) { try { return mPermissionManager diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index e84c5e574713..945957738f8e 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -677,4 +677,10 @@ interface IActivityManager { * Return whether the app freezer is supported (true) or not (false) by this system. */ boolean isAppFreezerSupported(); + + + /** + * Kills uid with the reason of permission change. + */ + void killUidForPermissionChange(int appId, int userId, String reason); } diff --git a/core/java/android/app/UiAutomationConnection.java b/core/java/android/app/UiAutomationConnection.java index 82e988109db8..ce51dba76780 100644 --- a/core/java/android/app/UiAutomationConnection.java +++ b/core/java/android/app/UiAutomationConnection.java @@ -294,7 +294,7 @@ public final class UiAutomationConnection extends IUiAutomationConnection.Stub { } final long identity = Binder.clearCallingIdentity(); try { - mPermissionManager.revokeRuntimePermission(packageName, permission, userId); + mPermissionManager.revokeRuntimePermission(packageName, permission, userId, null); } finally { Binder.restoreCallingIdentity(identity); } diff --git a/core/java/android/hardware/camera2/CameraDevice.java b/core/java/android/hardware/camera2/CameraDevice.java index 30ee32604939..15625cdeb8f4 100644 --- a/core/java/android/hardware/camera2/CameraDevice.java +++ b/core/java/android/hardware/camera2/CameraDevice.java @@ -680,7 +680,7 @@ public abstract class CameraDevice implements AutoCloseable { * </table><br> * </p> * - *<p>Devices capable of streaming concurrently with other devices as described by + *<p>BACKWARD_COMPATIBLE devices capable of streaming concurrently with other devices as described by * {@link android.hardware.camera2.CameraManager#getConcurrentCameraIds} have the * following guaranteed streams (when streaming concurrently with other devices)</p> * @@ -696,10 +696,14 @@ public abstract class CameraDevice implements AutoCloseable { * </table><br> * </p> * + * <p> Devices which are not backwards-compatible, support a mandatory single stream of size sVGA with image format {@code DEPTH16} during concurrent operation. + * * <p> For guaranteed concurrent stream configurations:</p> - * <p> s720p refers to the camera device's resolution for that format from {@link StreamConfigurationMap#getOutputSizes} or + * <p> sVGA refers to the camera device's maximum resolution for that format from {@link StreamConfigurationMap#getOutputSizes} or + * VGA resolution (640X480) whichever is lower. </p> + * <p> s720p refers to the camera device's maximum resolution for that format from {@link StreamConfigurationMap#getOutputSizes} or * 720p(1280X720) whichever is lower. </p> - * <p> s1440p refers to the camera device's resolution for that format from {@link StreamConfigurationMap#getOutputSizes} or + * <p> s1440p refers to the camera device's maximum resolution for that format from {@link StreamConfigurationMap#getOutputSizes} or * 1440p(1920X1440) whichever is lower. </p> * <p>MONOCHROME-capability ({@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES} * includes {@link CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_MONOCHROME MONOCHROME}) devices @@ -707,6 +711,7 @@ public abstract class CameraDevice implements AutoCloseable { * streams with {@code Y8} in all guaranteed stream combinations for the device's hardware level * and capabilities.</p> * + * * <p>Devices capable of outputting HEIC formats ({@link StreamConfigurationMap#getOutputFormats} * contains {@link android.graphics.ImageFormat#HEIC}) will support substituting {@code JPEG} * streams with {@code HEIC} in all guaranteed stream combinations for the device's hardware diff --git a/core/java/android/hardware/camera2/params/MandatoryStreamCombination.java b/core/java/android/hardware/camera2/params/MandatoryStreamCombination.java index 20d9c30bb4cc..776d155e5b3e 100644 --- a/core/java/android/hardware/camera2/params/MandatoryStreamCombination.java +++ b/core/java/android/hardware/camera2/params/MandatoryStreamCombination.java @@ -685,6 +685,12 @@ public final class MandatoryStreamCombination { "Standard still image capture"), }; + private static StreamCombinationTemplate sConcurrentDepthOnlyStreamCombinations[] = { + new StreamCombinationTemplate(new StreamTemplate [] { + new StreamTemplate(ImageFormat.DEPTH16, SizeThreshold.VGA) }, + "Depth capture for mesh based object rendering"), + }; + /** * Helper builder class to generate a list of available mandatory stream combinations. * @hide @@ -729,19 +735,21 @@ public final class MandatoryStreamCombination { getAvailableMandatoryConcurrentStreamCombinations() { // Since concurrent streaming support is optional, we mandate these stream // combinations regardless of camera device capabilities. + + StreamCombinationTemplate []chosenStreamCombinations = sConcurrentStreamCombinations; if (!isColorOutputSupported()) { - Log.v(TAG, "Device is not backward compatible!"); - throw new IllegalArgumentException("Camera device which is not BACKWARD_COMPATIBLE" - + " cannot have mandatory concurrent streams"); + Log.v(TAG, "Device is not backward compatible, depth streams are mandatory!"); + chosenStreamCombinations = sConcurrentDepthOnlyStreamCombinations; } + Size sizeVGAp = new Size(640, 480); Size size720p = new Size(1280, 720); Size size1440p = new Size(1920, 1440); ArrayList<MandatoryStreamCombination> availableConcurrentStreamCombinations = new ArrayList<MandatoryStreamCombination>(); availableConcurrentStreamCombinations.ensureCapacity( - sConcurrentStreamCombinations.length); - for (StreamCombinationTemplate combTemplate : sConcurrentStreamCombinations) { + chosenStreamCombinations.length); + for (StreamCombinationTemplate combTemplate : chosenStreamCombinations) { ArrayList<MandatoryStreamInformation> streamsInfo = new ArrayList<MandatoryStreamInformation>(); streamsInfo.ensureCapacity(combTemplate.mStreamTemplates.length); @@ -753,6 +761,9 @@ public final class MandatoryStreamCombination { case s1440p: formatSize = size1440p; break; + case VGA: + formatSize = sizeVGAp; + break; default: formatSize = size720p; } diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index d8b1f41c86d5..5647bf90d2fb 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -471,6 +471,10 @@ public class InputMethodService extends AbstractInputMethodService { final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer = info -> { onComputeInsets(mTmpInsets); + if (!mViewsCreated) { + // The IME views are not ready, keep visible insets untouched. + mTmpInsets.visibleTopInsets = 0; + } if (isExtractViewShown()) { // In true fullscreen mode, we just say the window isn't covering // any content so we don't impact whatever is behind. diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java index ef2a8a122e95..b36aeb89c02a 100755 --- a/core/java/android/os/Build.java +++ b/core/java/android/os/Build.java @@ -133,12 +133,23 @@ public class Build { * <a href="/training/articles/security-key-attestation.html">key attestation</a> to obtain * proof of the device's original identifiers. * - * <p>Requires Permission: READ_PRIVILEGED_PHONE_STATE, for the calling app to be the device or - * profile owner and have the READ_PHONE_STATE permission, or that the calling app has carrier - * privileges (see {@link android.telephony.TelephonyManager#hasCarrierPrivileges}). The profile - * owner is an app that owns a managed profile on the device; for more details see <a - * href="https://developer.android.com/work/managed-profiles">Work profiles</a>. Profile owner - * access is deprecated and will be removed in a future release. + * <p>Starting with API level 29, persistent device identifiers are guarded behind additional + * restrictions, and apps are recommended to use resettable identifiers (see <a + * href="c"> Best practices for unique identifiers</a>). This method can be invoked if one of + * the following requirements is met: + * <ul> + * <li>If the calling app has been granted the READ_PRIVILEGED_PHONE_STATE permission; this + * is a privileged permission that can only be granted to apps preloaded on the device. + * <li>If the calling app is the device or profile owner and has been granted the + * {@link Manifest.permission#READ_PHONE_STATE} permission. The profile owner is an app that + * owns a managed profile on the device; for more details see <a + * href="https://developer.android.com/work/managed-profiles">Work profiles</a>. + * Profile owner access is deprecated and will be removed in a future release. + * <li>If the calling app has carrier privileges (see {@link + * android.telephony.TelephonyManager#hasCarrierPrivileges}) on any active subscription. + * <li>If the calling app is the default SMS role holder (see {@link + * android.app.role.RoleManager#isRoleHeld(String)}). + * </ul> * * <p>If the calling app does not meet one of these requirements then this method will behave * as follows: @@ -150,7 +161,7 @@ public class Build { * the READ_PHONE_STATE permission, or if the calling app is targeting API level 29 or * higher, then a SecurityException is thrown.</li> * </ul> - * * + * * @return The serial number if specified. */ @SuppressAutoDoc // No support for device / profile owner. diff --git a/core/java/android/permission/IPermissionManager.aidl b/core/java/android/permission/IPermissionManager.aidl index 235b0830b9aa..e23102113e9f 100644 --- a/core/java/android/permission/IPermissionManager.aidl +++ b/core/java/android/permission/IPermissionManager.aidl @@ -71,7 +71,7 @@ interface IPermissionManager { void grantRuntimePermission(String packageName, String permName, int userId); - void revokeRuntimePermission(String packageName, String permName, int userId); + void revokeRuntimePermission(String packageName, String permName, int userId, String reason); void resetRuntimePermissions(); diff --git a/core/java/android/service/autofill/IInlineSuggestionRenderService.aidl b/core/java/android/service/autofill/IInlineSuggestionRenderService.aidl index bf0bb9e2a41f..7cd372fe97d8 100644 --- a/core/java/android/service/autofill/IInlineSuggestionRenderService.aidl +++ b/core/java/android/service/autofill/IInlineSuggestionRenderService.aidl @@ -29,6 +29,12 @@ import android.service.autofill.InlinePresentation; oneway interface IInlineSuggestionRenderService { void renderSuggestion(in IInlineSuggestionUiCallback callback, in InlinePresentation presentation, int width, int height, - in IBinder hostInputToken, int displayId); + in IBinder hostInputToken, int displayId, int userId, int sessionId); void getInlineSuggestionsRendererInfo(in RemoteCallback callback); + + /** + * Releases the inline suggestion SurfaceControlViewHosts hosted in the service, for the + * provided userId and sessionId. + */ + void destroySuggestionViews(int userId, int sessionId); } diff --git a/core/java/android/service/autofill/InlineSuggestionRenderService.java b/core/java/android/service/autofill/InlineSuggestionRenderService.java index 8790fb2299f5..839caff5c3d4 100644 --- a/core/java/android/service/autofill/InlineSuggestionRenderService.java +++ b/core/java/android/service/autofill/InlineSuggestionRenderService.java @@ -41,6 +41,8 @@ import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; +import java.io.FileDescriptor; +import java.io.PrintWriter; import java.lang.ref.WeakReference; /** @@ -82,7 +84,7 @@ public abstract class InlineSuggestionRenderService extends Service { Boolean newValue) { if (evicted) { Log.w(TAG, - "Hit max=100 entries in the cache. Releasing oldest one to make " + "Hit max=30 entries in the cache. Releasing oldest one to make " + "space."); key.releaseSurfaceControlViewHost(); } @@ -130,7 +132,7 @@ public abstract class InlineSuggestionRenderService extends Service { private void handleRenderSuggestion(IInlineSuggestionUiCallback callback, InlinePresentation presentation, int width, int height, IBinder hostInputToken, - int displayId) { + int displayId, int userId, int sessionId) { if (hostInputToken == null) { try { callback.onError(); @@ -192,7 +194,8 @@ public abstract class InlineSuggestionRenderService extends Service { } return true; }); - final InlineSuggestionUiImpl uiImpl = new InlineSuggestionUiImpl(host, mMainHandler); + final InlineSuggestionUiImpl uiImpl = new InlineSuggestionUiImpl(host, mMainHandler, + userId, sessionId); mActiveInlineSuggestions.put(uiImpl, true); // We post the callback invocation to the end of the main thread handler queue, to make @@ -218,6 +221,18 @@ public abstract class InlineSuggestionRenderService extends Service { callback.sendResult(rendererInfo); } + private void handleDestroySuggestionViews(int userId, int sessionId) { + Log.v(TAG, "handleDestroySuggestionViews called for " + userId + ":" + sessionId); + for (final InlineSuggestionUiImpl inlineSuggestionUi : + mActiveInlineSuggestions.snapshot().keySet()) { + if (inlineSuggestionUi.mUserId == userId + && inlineSuggestionUi.mSessionId == sessionId) { + Log.v(TAG, "Destroy " + inlineSuggestionUi); + inlineSuggestionUi.releaseSurfaceControlViewHost(); + } + } + } + /** * A wrapper class around the {@link InlineSuggestionUiImpl} to ensure it's not strongly * reference by the remote system server process. @@ -260,10 +275,15 @@ public abstract class InlineSuggestionRenderService extends Service { private SurfaceControlViewHost mViewHost; @NonNull private final Handler mHandler; + private final int mUserId; + private final int mSessionId; - InlineSuggestionUiImpl(SurfaceControlViewHost viewHost, Handler handler) { + InlineSuggestionUiImpl(SurfaceControlViewHost viewHost, Handler handler, int userId, + int sessionId) { this.mViewHost = viewHost; this.mHandler = handler; + this.mUserId = userId; + this.mSessionId = sessionId; } /** @@ -302,6 +322,16 @@ public abstract class InlineSuggestionRenderService extends Service { } } + /** @hide */ + @Override + protected final void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, + @NonNull String[] args) { + pw.println("mActiveInlineSuggestions: " + mActiveInlineSuggestions.size()); + for (InlineSuggestionUiImpl impl : mActiveInlineSuggestions.snapshot().keySet()) { + pw.printf("ui: [%s] - [%d] [%d]\n", impl, impl.mUserId, impl.mSessionId); + } + } + @Override @Nullable public final IBinder onBind(@NonNull Intent intent) { @@ -311,11 +341,12 @@ public abstract class InlineSuggestionRenderService extends Service { @Override public void renderSuggestion(@NonNull IInlineSuggestionUiCallback callback, @NonNull InlinePresentation presentation, int width, int height, - @Nullable IBinder hostInputToken, int displayId) { + @Nullable IBinder hostInputToken, int displayId, int userId, + int sessionId) { mMainHandler.sendMessage( obtainMessage(InlineSuggestionRenderService::handleRenderSuggestion, InlineSuggestionRenderService.this, callback, presentation, - width, height, hostInputToken, displayId)); + width, height, hostInputToken, displayId, userId, sessionId)); } @Override @@ -324,6 +355,12 @@ public abstract class InlineSuggestionRenderService extends Service { InlineSuggestionRenderService::handleGetInlineSuggestionsRendererInfo, InlineSuggestionRenderService.this, callback)); } + @Override + public void destroySuggestionViews(int userId, int sessionId) { + mMainHandler.sendMessage(obtainMessage( + InlineSuggestionRenderService::handleDestroySuggestionViews, + InlineSuggestionRenderService.this, userId, sessionId)); + } }.asBinder(); } diff --git a/core/java/android/service/controls/ControlsProviderService.java b/core/java/android/service/controls/ControlsProviderService.java index 4e5aa0018b61..6bd376a19fc5 100644 --- a/core/java/android/service/controls/ControlsProviderService.java +++ b/core/java/android/service/controls/ControlsProviderService.java @@ -296,6 +296,10 @@ public abstract class ControlsProviderService extends Service { /** * Request SystemUI to prompt the user to add a control to favorites. + * <br> + * SystemUI may not honor this request in some cases, for example if the requested + * {@link Control} is already a favorite, or the requesting package is not currently in the + * foreground. * * @param context A context * @param componentName Component name of the {@link ControlsProviderService} diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index a3c88bacb627..c6be91fa1bf5 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -514,6 +514,12 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation /** Set of inset types for which an animation was started since last resetting this field */ private @InsetsType int mLastStartedAnimTypes; + /** Set of inset types which cannot be controlled by the user animation */ + private @InsetsType int mDisabledUserAnimationInsetsTypes; + + private Runnable mInvokeControllableInsetsChangedListeners = + this::invokeControllableInsetsChangedListeners; + public InsetsController(Host host) { this(host, (controller, type) -> { if (type == ITYPE_IME) { @@ -628,22 +634,57 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation private void updateState(InsetsState newState) { mState.setDisplayFrame(newState.getDisplayFrame()); - for (int i = 0; i < InsetsState.SIZE; i++) { - InsetsSource source = newState.peekSource(i); - if (source == null) continue;; - getSourceConsumer(source.getType()).updateSource(source); + @InsetsType int disabledUserAnimationTypes = 0; + @InsetsType int[] cancelledUserAnimationTypes = {0}; + for (@InternalInsetsType int type = 0; type < InsetsState.SIZE; type++) { + InsetsSource source = newState.peekSource(type); + if (source == null) continue; + @AnimationType int animationType = getAnimationType(type); + if (!source.isUserControllable()) { + @InsetsType int insetsType = toPublicType(type); + // The user animation is not allowed when visible frame is empty. + disabledUserAnimationTypes |= insetsType; + if (animationType == ANIMATION_TYPE_USER) { + // Existing user animation needs to be cancelled. + animationType = ANIMATION_TYPE_NONE; + cancelledUserAnimationTypes[0] |= insetsType; + } + } + getSourceConsumer(type).updateSource(source, animationType); } - for (int i = 0; i < InsetsState.SIZE; i++) { - InsetsSource source = mState.peekSource(i); + for (@InternalInsetsType int type = 0; type < InsetsState.SIZE; type++) { + InsetsSource source = mState.peekSource(type); if (source == null) continue; - if (newState.peekSource(source.getType()) == null) { - mState.removeSource(source.getType()); + if (newState.peekSource(type) == null) { + mState.removeSource(type); } } if (mCaptionInsetsHeight != 0) { mState.getSource(ITYPE_CAPTION_BAR).setFrame(new Rect(mFrame.left, mFrame.top, mFrame.right, mFrame.top + mCaptionInsetsHeight)); } + + updateDisabledUserAnimationTypes(disabledUserAnimationTypes); + + if (cancelledUserAnimationTypes[0] != 0) { + mHandler.post(() -> show(cancelledUserAnimationTypes[0])); + } + } + + private void updateDisabledUserAnimationTypes(@InsetsType int disabledUserAnimationTypes) { + @InsetsType int diff = mDisabledUserAnimationInsetsTypes ^ disabledUserAnimationTypes; + if (diff != 0) { + for (int i = mSourceConsumers.size() - 1; i >= 0; i--) { + InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i); + if (consumer.getControl() != null + && (toPublicType(consumer.getType()) & diff) != 0) { + mHandler.removeCallbacks(mInvokeControllableInsetsChangedListeners); + mHandler.post(mInvokeControllableInsetsChangedListeners); + break; + } + } + mDisabledUserAnimationInsetsTypes = disabledUserAnimationTypes; + } } private boolean captionInsetsUnchanged() { @@ -847,6 +888,18 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation + " while an existing " + Type.toString(mTypesBeingCancelled) + " is being cancelled."); } + if (animationType == ANIMATION_TYPE_USER) { + final @InsetsType int disabledTypes = types & mDisabledUserAnimationInsetsTypes; + if (DEBUG) Log.d(TAG, "user animation disabled types: " + disabledTypes); + types &= ~mDisabledUserAnimationInsetsTypes; + + if (fromIme && (disabledTypes & ime()) != 0 + && !mState.getSource(ITYPE_IME).isVisible()) { + // We've requested IMM to show IME, but the IME is not controllable. We need to + // cancel the request. + getSourceConsumer(ITYPE_IME).hide(true, animationType); + } + } if (types == 0) { // nothing to animate. listener.onCancelled(null); @@ -1320,7 +1373,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation @InsetsType int result = 0; for (int i = mSourceConsumers.size() - 1; i >= 0; i--) { InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i); - if (consumer.getControl() != null) { + InsetsSource source = mState.peekSource(consumer.mType); + if (consumer.getControl() != null && source != null && source.isUserControllable()) { result |= toPublicType(consumer.mType); } } @@ -1331,6 +1385,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation * @return The types that are now animating due to a listener invoking control/show/hide */ private @InsetsType int invokeControllableInsetsChangedListeners() { + mHandler.removeCallbacks(mInvokeControllableInsetsChangedListeners); mLastStartedAnimTypes = 0; @InsetsType int types = calculateControllableTypes(); int size = mControllableInsetsChangedListeners.size(); diff --git a/core/java/android/view/InsetsSource.java b/core/java/android/view/InsetsSource.java index 15b9a9330392..dbf75705c073 100644 --- a/core/java/android/view/InsetsSource.java +++ b/core/java/android/view/InsetsSource.java @@ -92,6 +92,11 @@ public class InsetsSource implements Parcelable { return mVisible; } + boolean isUserControllable() { + // If mVisibleFrame is null, it will be the same area as mFrame. + return mVisibleFrame == null || !mVisibleFrame.isEmpty(); + } + /** * Calculates the insets this source will cause to a client window. * diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java index b62e67c8f9e1..40e6f4b2fce8 100644 --- a/core/java/android/view/InsetsSourceConsumer.java +++ b/core/java/android/view/InsetsSourceConsumer.java @@ -18,8 +18,8 @@ package android.view; import static android.view.InsetsController.ANIMATION_TYPE_NONE; import static android.view.InsetsController.AnimationType; -import static android.view.InsetsState.getDefaultVisibility; import static android.view.InsetsController.DEBUG; +import static android.view.InsetsState.getDefaultVisibility; import static android.view.InsetsState.toPublicType; import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; @@ -284,9 +284,9 @@ public class InsetsSourceConsumer { } @VisibleForTesting(visibility = PACKAGE) - public void updateSource(InsetsSource newSource) { + public void updateSource(InsetsSource newSource, @AnimationType int animationType) { InsetsSource source = mState.peekSource(mType); - if (source == null || mController.getAnimationType(mType) == ANIMATION_TYPE_NONE + if (source == null || animationType == ANIMATION_TYPE_NONE || source.getFrame().equals(newSource.getFrame())) { mPendingFrame = null; mPendingVisibleFrame = null; @@ -295,7 +295,7 @@ public class InsetsSourceConsumer { } // Frame is changing while animating. Keep note of the new frame but keep existing frame - // until animaition is finished. + // until animation is finished. newSource = new InsetsSource(newSource); mPendingFrame = new Rect(newSource.getFrame()); mPendingVisibleFrame = newSource.getVisibleFrame() != null diff --git a/core/java/android/view/InsetsState.java b/core/java/android/view/InsetsState.java index 9bf2e01a6bd1..91e7591193f1 100644 --- a/core/java/android/view/InsetsState.java +++ b/core/java/android/view/InsetsState.java @@ -427,8 +427,7 @@ public class InsetsState implements Parcelable { if (copySources) { for (int i = 0; i < SIZE; i++) { InsetsSource source = other.mSources[i]; - if (source == null) continue; - mSources[i] = new InsetsSource(source); + mSources[i] = source != null ? new InsetsSource(source) : null; } } else { for (int i = 0; i < SIZE; i++) { diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java index 19eff72ca814..51b0c6b59f3c 100644 --- a/core/java/android/view/MotionEvent.java +++ b/core/java/android/view/MotionEvent.java @@ -487,6 +487,21 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int FLAG_TAINTED = 0x80000000; /** + * Private flag indicating that this event was synthesized by the system and should be delivered + * to the accessibility focused view first. When being dispatched such an event is not handled + * by predecessors of the accessibility focused view and after the event reaches that view the + * flag is cleared and normal event dispatch is performed. This ensures that the platform can + * click on any view that has accessibility focus which is semantically equivalent to asking the + * view to perform a click accessibility action but more generic as views not implementing click + * action correctly can still be activated. + * + * @hide + * @see #isTargetAccessibilityFocus() + * @see #setTargetAccessibilityFocus(boolean) + */ + public static final int FLAG_TARGET_ACCESSIBILITY_FOCUS = 0x40000000; + + /** * Flag indicating the motion event intersected the top edge of the screen. */ public static final int EDGE_TOP = 0x00000001; @@ -2140,6 +2155,20 @@ public final class MotionEvent extends InputEvent implements Parcelable { } /** @hide */ + public boolean isTargetAccessibilityFocus() { + final int flags = getFlags(); + return (flags & FLAG_TARGET_ACCESSIBILITY_FOCUS) != 0; + } + + /** @hide */ + public void setTargetAccessibilityFocus(boolean targetsFocus) { + final int flags = getFlags(); + nativeSetFlags(mNativePtr, targetsFocus + ? flags | FLAG_TARGET_ACCESSIBILITY_FOCUS + : flags & ~FLAG_TARGET_ACCESSIBILITY_FOCUS); + } + + /** @hide */ public final boolean isHoverExitPending() { final int flags = getFlags(); return (flags & FLAG_HOVER_EXIT_PENDING) != 0; diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java index 3b3836582b16..daeb1c9c1e01 100644 --- a/core/java/android/view/SurfaceControl.java +++ b/core/java/android/view/SurfaceControl.java @@ -63,8 +63,10 @@ import dalvik.system.CloseGuard; import libcore.util.NativeAllocationRegistry; import java.io.Closeable; +import java.lang.ref.WeakReference; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.util.ArrayList; import java.util.Objects; /** @@ -226,24 +228,86 @@ public final class SurfaceControl implements Parcelable { private static native void nativeSetFixedTransformHint(long transactionObj, long nativeObject, int transformHint); + @Nullable + @GuardedBy("mLock") + private ArrayList<OnReparentListener> mReparentListeners; + + /** + * Listener to observe surface reparenting. + * + * @hide + */ + public interface OnReparentListener { + + /** + * Callback for reparenting surfaces. + * + * Important: You should only interact with the provided surface control + * only if you have a contract with its owner to avoid them closing it + * under you or vise versa. + * + * @param transaction The transaction that would commit reparenting. + * @param parent The future parent surface. + */ + void onReparent(@NonNull Transaction transaction, @Nullable SurfaceControl parent); + } + private final CloseGuard mCloseGuard = CloseGuard.get(); private String mName; - /** + + /** * @hide */ public long mNativeObject; private long mNativeHandle; - // TODO: Move this to native. - private final Object mSizeLock = new Object(); - @GuardedBy("mSizeLock") + // TODO: Move width/height to native and fix locking through out. + private final Object mLock = new Object(); + @GuardedBy("mLock") private int mWidth; - @GuardedBy("mSizeLock") + @GuardedBy("mLock") private int mHeight; + private WeakReference<View> mLocalOwnerView; + static Transaction sGlobalTransaction; static long sTransactionNestCount = 0; + /** + * Adds a reparenting listener. + * + * @param listener The listener. + * @return Whether listener was added. + * + * @hide + */ + public boolean addOnReparentListener(@NonNull OnReparentListener listener) { + synchronized (mLock) { + if (mReparentListeners == null) { + mReparentListeners = new ArrayList<>(1); + } + return mReparentListeners.add(listener); + } + } + + /** + * Removes a reparenting listener. + * + * @param listener The listener. + * @return Whether listener was removed. + * + * @hide + */ + public boolean removeOnReparentListener(@NonNull OnReparentListener listener) { + synchronized (mLock) { + final boolean removed = mReparentListeners.remove(listener); + if (mReparentListeners.isEmpty()) { + mReparentListeners = null; + } + return removed; + } + } + /* flags used in constructor (keep in sync with ISurfaceComposerClient.h) */ /** @@ -455,6 +519,7 @@ public final class SurfaceControl implements Parcelable { mName = other.mName; mWidth = other.mWidth; mHeight = other.mHeight; + mLocalOwnerView = other.mLocalOwnerView; assignNativeObject(nativeCopyFromSurfaceControl(other.mNativeObject)); } @@ -553,6 +618,7 @@ public final class SurfaceControl implements Parcelable { private int mHeight; private int mFormat = PixelFormat.OPAQUE; private String mName; + private WeakReference<View> mLocalOwnerView; private SurfaceControl mParent; private SparseIntArray mMetadata; @@ -587,7 +653,8 @@ public final class SurfaceControl implements Parcelable { "Only buffer layers can set a valid buffer size."); } return new SurfaceControl( - mSession, mName, mWidth, mHeight, mFormat, mFlags, mParent, mMetadata); + mSession, mName, mWidth, mHeight, mFormat, mFlags, mParent, mMetadata, + mLocalOwnerView); } /** @@ -602,6 +669,27 @@ public final class SurfaceControl implements Parcelable { } /** + * Set the local owner view for the surface. This view is only + * valid in the same process and is not transferred in an IPC. + * + * Note: This is used for cases where we want to know the view + * that manages the surface control while intercepting reparenting. + * A specific example is InlineContentView which exposes is surface + * control for reparenting as a way to implement clipping of several + * InlineContentView instances within a certain area. + * + * @param view The owner view. + * @return This builder. + * + * @hide + */ + @NonNull + public Builder setLocalOwnerView(@NonNull View view) { + mLocalOwnerView = new WeakReference<>(view); + return this; + } + + /** * Set the initial size of the controlled surface's buffers in pixels. * * @param width The buffer width in pixels. @@ -858,7 +946,7 @@ public final class SurfaceControl implements Parcelable { * @throws throws OutOfResourcesException If the SurfaceControl cannot be created. */ private SurfaceControl(SurfaceSession session, String name, int w, int h, int format, int flags, - SurfaceControl parent, SparseIntArray metadata) + SurfaceControl parent, SparseIntArray metadata, WeakReference<View> localOwnerView) throws OutOfResourcesException, IllegalArgumentException { if (name == null) { throw new IllegalArgumentException("name must not be null"); @@ -867,6 +955,7 @@ public final class SurfaceControl implements Parcelable { mName = name; mWidth = w; mHeight = h; + mLocalOwnerView = localOwnerView; Parcel metaParcel = Parcel.obtain(); try { if (metadata != null && metadata.size() > 0) { @@ -1307,7 +1396,7 @@ public final class SurfaceControl implements Parcelable { * @hide */ public int getWidth() { - synchronized (mSizeLock) { + synchronized (mLock) { return mWidth; } } @@ -1316,11 +1405,22 @@ public final class SurfaceControl implements Parcelable { * @hide */ public int getHeight() { - synchronized (mSizeLock) { + synchronized (mLock) { return mHeight; } } + /** + * Gets the local view that owns this surface. + * + * @return The owner view. + * + * @hide + */ + public @Nullable View getLocalOwnerView() { + return (mLocalOwnerView != null) ? mLocalOwnerView.get() : null; + } + @Override public String toString() { return "Surface(name=" + mName + ")/@0x" + @@ -2165,6 +2265,9 @@ public final class SurfaceControl implements Parcelable { public long mNativeObject; private final ArrayMap<SurfaceControl, Point> mResizedSurfaces = new ArrayMap<>(); + private final ArrayMap<SurfaceControl, SurfaceControl> mReparentedSurfaces = + new ArrayMap<>(); + Runnable mFreeNativeResources; private static final float[] INVALID_COLOR = {-1, -1, -1}; @@ -2205,6 +2308,8 @@ public final class SurfaceControl implements Parcelable { */ @Override public void close() { + mResizedSurfaces.clear(); + mReparentedSurfaces.clear(); mFreeNativeResources.run(); mNativeObject = 0; } @@ -2215,6 +2320,7 @@ public final class SurfaceControl implements Parcelable { */ public void apply(boolean sync) { applyResizedSurfaces(); + notifyReparentedSurfaces(); nativeApplyTransaction(mNativeObject, sync); } @@ -2222,7 +2328,7 @@ public final class SurfaceControl implements Parcelable { for (int i = mResizedSurfaces.size() - 1; i >= 0; i--) { final Point size = mResizedSurfaces.valueAt(i); final SurfaceControl surfaceControl = mResizedSurfaces.keyAt(i); - synchronized (surfaceControl.mSizeLock) { + synchronized (surfaceControl.mLock) { surfaceControl.mWidth = size.x; surfaceControl.mHeight = size.y; } @@ -2230,6 +2336,22 @@ public final class SurfaceControl implements Parcelable { mResizedSurfaces.clear(); } + private void notifyReparentedSurfaces() { + final int reparentCount = mReparentedSurfaces.size(); + for (int i = reparentCount - 1; i >= 0; i--) { + final SurfaceControl child = mReparentedSurfaces.keyAt(i); + synchronized (child.mLock) { + final int listenerCount = (child.mReparentListeners != null) + ? child.mReparentListeners.size() : 0; + for (int j = 0; j < listenerCount; j++) { + final OnReparentListener listener = child.mReparentListeners.get(j); + listener.onReparent(this, mReparentedSurfaces.valueAt(i)); + } + mReparentedSurfaces.removeAt(i); + } + } + } + /** * Toggle the visibility of a given Layer and it's sub-tree. * @@ -2632,6 +2754,7 @@ public final class SurfaceControl implements Parcelable { otherObject = newParent.mNativeObject; } nativeReparent(mNativeObject, sc.mNativeObject, otherObject); + mReparentedSurfaces.put(sc, newParent); return this; } @@ -2912,6 +3035,8 @@ public final class SurfaceControl implements Parcelable { } mResizedSurfaces.putAll(other.mResizedSurfaces); other.mResizedSurfaces.clear(); + mReparentedSurfaces.putAll(other.mReparentedSurfaces); + other.mReparentedSurfaces.clear(); nativeMergeTransaction(mNativeObject, other.mNativeObject); return this; } diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java index 90e1eab09fd6..0d21eb5cf920 100644 --- a/core/java/android/view/SurfaceView.java +++ b/core/java/android/view/SurfaceView.java @@ -44,7 +44,6 @@ import android.os.SystemClock; import android.util.AttributeSet; import android.util.Log; import android.view.SurfaceControl.Transaction; -import android.view.SurfaceControlViewHost; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.IAccessibilityEmbeddedConnection; @@ -988,6 +987,7 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession) .setName(name) + .setLocalOwnerView(this) .setOpaque((mSurfaceFlags & SurfaceControl.OPAQUE) != 0) .setBufferSize(mSurfaceWidth, mSurfaceHeight) .setFormat(mFormat) @@ -996,6 +996,7 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall .build(); mBackgroundControl = new SurfaceControl.Builder(mSurfaceSession) .setName("Background for -" + name) + .setLocalOwnerView(this) .setOpaque(true) .setColorLayer() .setParent(mSurfaceControl) @@ -1051,11 +1052,12 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall // we still need to latch a buffer). // b/28866173 if (sizeChanged || creating || !mRtHandlingPositionUpdates) { - mTmpTransaction.setPosition(mSurfaceControl, mScreenRect.left, - mScreenRect.top); - mTmpTransaction.setMatrix(mSurfaceControl, - mScreenRect.width() / (float) mSurfaceWidth, 0.0f, 0.0f, - mScreenRect.height() / (float) mSurfaceHeight); + onSetSurfacePositionAndScaleRT(mTmpTransaction, mSurfaceControl, + mScreenRect.left, /*positionLeft*/ + mScreenRect.top /*positionTop*/ , + mScreenRect.width() / (float) mSurfaceWidth /*postScaleX*/, + mScreenRect.height() / (float) mSurfaceHeight /*postScaleY*/); + // Set a window crop when creating the surface or changing its size to // crop the buffer to the surface size since the buffer producer may // use SCALING_MODE_SCALE and submit a larger size than the surface @@ -1211,6 +1213,40 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall Surface viewRootSurface, long nextViewRootFrameNumber) { } + /** + * Sets the surface position and scale. Can be called on + * the UI thread as well as on the renderer thread. + * + * @param transaction Transaction in which to execute. + * @param surface Surface whose location to set. + * @param positionLeft The left position to set. + * @param positionTop The top position to set. + * @param postScaleX The X axis post scale + * @param postScaleY The Y axis post scale + * + * @hide + */ + protected void onSetSurfacePositionAndScaleRT(@NonNull Transaction transaction, + @NonNull SurfaceControl surface, int positionLeft, int positionTop, + float postScaleX, float postScaleY) { + transaction.setPosition(surface, positionLeft, positionTop); + transaction.setMatrix(surface, postScaleX /*dsdx*/, 0f /*dtdx*/, + 0f /*dtdy*/, postScaleY /*dsdy*/); + } + + /** @hide */ + public void requestUpdateSurfacePositionAndScale() { + if (mSurfaceControl == null) { + return; + } + onSetSurfacePositionAndScaleRT(mTmpTransaction, mSurfaceControl, + mScreenRect.left, /*positionLeft*/ + mScreenRect.top/*positionTop*/ , + mScreenRect.width() / (float) mSurfaceWidth /*postScaleX*/, + mScreenRect.height() / (float) mSurfaceHeight /*postScaleY*/); + mTmpTransaction.apply(); + } + private void applySurfaceTransforms(SurfaceControl surface, SurfaceControl.Transaction t, Rect position, long frameNumber) { final ViewRootImpl viewRoot = getViewRootImpl(); @@ -1219,16 +1255,26 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall frameNumber); } - t.setPosition(surface, position.left, position.top); - t.setMatrix(surface, - position.width() / (float) mSurfaceWidth, - 0.0f, 0.0f, - position.height() / (float) mSurfaceHeight); + onSetSurfacePositionAndScaleRT(t, surface, + position.left /*positionLeft*/, + position.top /*positionTop*/, + position.width() / (float) mSurfaceWidth /*postScaleX*/, + position.height() / (float) mSurfaceHeight /*postScaleY*/); + if (mViewVisibility) { t.show(surface); } } + /** + * @return The last render position of the backing surface or an empty rect. + * + * @hide + */ + public @NonNull Rect getSurfaceRenderPosition() { + return mRTLastReportedPosition; + } + private void setParentSpaceRectangle(Rect position, long frameNumber) { final ViewRootImpl viewRoot = getViewRootImpl(); final boolean useBLAST = viewRoot.isDrawingToBLASTTransaction(); diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 1226202dfdf9..df1c672eb9eb 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -14274,6 +14274,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public boolean dispatchTouchEvent(MotionEvent event) { // If the event should be handled by accessibility focus first. + if (event.isTargetAccessibilityFocus()) { + // We don't have focus or no virtual descendant has it, do not handle the event. + if (!isAccessibilityFocusedViewOrHost()) { + return false; + } + // We have focus and got the event, then use normal event dispatch. + event.setTargetAccessibilityFocus(false); + } boolean result = false; if (mInputEventConsistencyVerifier != null) { diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index e3362aafbcd4..77fedd7c30d4 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -2048,8 +2048,26 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex); + View childWithAccessibilityFocus = + event.isTargetAccessibilityFocus() + ? findChildWithAccessibilityFocus() + : null; + if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) { + + // If there is a view that has accessibility focus we want it + // to get the event first and if not handled we will perform a + // normal dispatch. We may do a double iteration but this is + // safer given the timeframe. + if (childWithAccessibilityFocus != null) { + if (childWithAccessibilityFocus != child) { + continue; + } + childWithAccessibilityFocus = null; + i = childrenCount - 1; + } + event.setTargetAccessibilityFocus(false); continue; } final PointerIcon pointerIcon = @@ -2617,6 +2635,12 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager mInputEventConsistencyVerifier.onTouchEvent(ev, 1); } + // If the event targets the accessibility focused view and this is it, start + // normal event dispatch. Maybe a descendant is what will handle the click. + if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) { + ev.setTargetAccessibilityFocus(false); + } + boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); @@ -2647,6 +2671,13 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager // so this view group continues to intercept touches. intercepted = true; } + + // If intercepted, start normal event dispatch. Also if there is already + // a view that is handling the gesture, do normal event dispatch. + if (intercepted || mFirstTouchTarget != null) { + ev.setTargetAccessibilityFocus(false); + } + // Check for cancelation. final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL; @@ -2658,6 +2689,14 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager TouchTarget newTouchTarget = null; boolean alreadyDispatchedToNewTouchTarget = false; if (!canceled && !intercepted) { + // If the event is targeting accessibility focus we give it to the + // view that has accessibility focus and if it does not handle it + // we clear the flag and dispatch the event to all children as usual. + // We are looking up the accessibility focused host to avoid keeping + // state since these events are very rare. + View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus() + ? findChildWithAccessibilityFocus() : null; + if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { @@ -2720,6 +2759,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager alreadyDispatchedToNewTouchTarget = true; break; } + + // The accessibility focus didn't handle the event, so clear + // the flag and do a normal dispatch to all children. + ev.setTargetAccessibilityFocus(false); } if (preorderedList != null) preorderedList.clear(); } @@ -2803,6 +2846,34 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager return buildOrderedChildList(); } + /** + * Finds the child which has accessibility focus. + * + * @return The child that has focus. + */ + private View findChildWithAccessibilityFocus() { + ViewRootImpl viewRoot = getViewRootImpl(); + if (viewRoot == null) { + return null; + } + + View current = viewRoot.getAccessibilityFocusedHost(); + if (current == null) { + return null; + } + + ViewParent parent = current.getParent(); + while (parent instanceof View) { + if (parent == this) { + return current; + } + current = (View) parent; + parent = current.getParent(); + } + + return null; + } + /** * Resets all touch state in preparation for a new cycle. */ @@ -3257,9 +3328,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager break; } default: - throw new IllegalStateException("descendant focusability must be " - + "one of FOCUS_BEFORE_DESCENDANTS, FOCUS_AFTER_DESCENDANTS, FOCUS_BLOCK_DESCENDANTS " - + "but is " + descendantFocusability); + throw new IllegalStateException( + "descendant focusability must be one of FOCUS_BEFORE_DESCENDANTS," + + " FOCUS_AFTER_DESCENDANTS, FOCUS_BLOCK_DESCENDANTS but is " + + descendantFocusability); } if (result && !isLayoutValid() && ((mPrivateFlags & PFLAG_WANTS_FOCUS) == 0)) { mPrivateFlags |= PFLAG_WANTS_FOCUS; @@ -4925,7 +4997,8 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager if (params == null) { params = generateDefaultLayoutParams(); if (params == null) { - throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null"); + throw new IllegalArgumentException( + "generateDefaultLayoutParams() cannot return null"); } } addView(child, index, params); diff --git a/core/java/android/view/autofill/AutofillId.java b/core/java/android/view/autofill/AutofillId.java index 68943bf2a83a..32b9cf7cdbb0 100644 --- a/core/java/android/view/autofill/AutofillId.java +++ b/core/java/android/view/autofill/AutofillId.java @@ -73,6 +73,8 @@ public final class AutofillId implements Parcelable { } /** @hide */ + @NonNull + @TestApi public static AutofillId withoutSession(@NonNull AutofillId id) { final int flags = id.mFlags & ~FLAG_HAS_SESSION; final long virtualChildId = diff --git a/core/java/android/widget/inline/InlineContentView.java b/core/java/android/widget/inline/InlineContentView.java index 8ca218c1d1a7..9712311aab7c 100644 --- a/core/java/android/widget/inline/InlineContentView.java +++ b/core/java/android/widget/inline/InlineContentView.java @@ -18,17 +18,22 @@ package android.widget.inline; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.TestApi; import android.content.Context; import android.graphics.PixelFormat; +import android.graphics.PointF; +import android.graphics.Rect; import android.util.AttributeSet; import android.util.Log; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; import android.view.SurfaceHolder; import android.view.SurfaceView; +import android.view.View; import android.view.ViewGroup; -import android.view.ViewTreeObserver.OnPreDrawListener; +import android.view.ViewTreeObserver; +import java.lang.ref.WeakReference; import java.util.function.Consumer; /** @@ -87,8 +92,10 @@ public class InlineContentView extends ViewGroup { * * @hide */ + @TestApi public interface SurfacePackageUpdater { + /** * Called when the previous surface package is released due to view being detached * from the window. @@ -100,14 +107,16 @@ public class InlineContentView extends ViewGroup { * * @param consumer consumes the updated surface package. */ - void getSurfacePackage(Consumer<SurfaceControlViewHost.SurfacePackage> consumer); + void getSurfacePackage(@NonNull Consumer<SurfaceControlViewHost.SurfacePackage> consumer); } @NonNull private final SurfaceHolder.Callback mSurfaceCallback = new SurfaceHolder.Callback() { @Override public void surfaceCreated(@NonNull SurfaceHolder holder) { - mSurfaceControlCallback.onCreated(mSurfaceView.getSurfaceControl()); + final SurfaceControl surfaceControl = mSurfaceView.getSurfaceControl(); + surfaceControl.addOnReparentListener(mOnReparentListener); + mSurfaceControlCallback.onCreated(surfaceControl); } @Override @@ -118,7 +127,37 @@ public class InlineContentView extends ViewGroup { @Override public void surfaceDestroyed(@NonNull SurfaceHolder holder) { - mSurfaceControlCallback.onDestroyed(mSurfaceView.getSurfaceControl()); + final SurfaceControl surfaceControl = mSurfaceView.getSurfaceControl(); + surfaceControl.removeOnReparentListener(mOnReparentListener); + mSurfaceControlCallback.onDestroyed(surfaceControl); + } + }; + + @NonNull + private final SurfaceControl.OnReparentListener mOnReparentListener = + new SurfaceControl.OnReparentListener() { + @Override + public void onReparent(SurfaceControl.Transaction transaction, + SurfaceControl parent) { + final View parentSurfaceOwnerView = (parent != null) + ? parent.getLocalOwnerView() : null; + if (parentSurfaceOwnerView instanceof SurfaceView) { + mParentSurfaceOwnerView = new WeakReference<>( + (SurfaceView) parentSurfaceOwnerView); + } else { + mParentSurfaceOwnerView = null; + } + } + }; + + @NonNull + private final ViewTreeObserver.OnDrawListener mOnDrawListener = + new ViewTreeObserver.OnDrawListener() { + @Override + public void onDraw() { + computeParentPositionAndScale(); + final int visibility = InlineContentView.this.isShown() ? VISIBLE : GONE; + mSurfaceView.setVisibility(visibility); } }; @@ -126,21 +165,20 @@ public class InlineContentView extends ViewGroup { private final SurfaceView mSurfaceView; @Nullable + private WeakReference<SurfaceView> mParentSurfaceOwnerView; + + @Nullable + private int[] mParentPosition; + + @Nullable + private PointF mParentScale; + + @Nullable private SurfaceControlCallback mSurfaceControlCallback; @Nullable private SurfacePackageUpdater mSurfacePackageUpdater; - @NonNull - private final OnPreDrawListener mDrawListener = new OnPreDrawListener() { - @Override - public boolean onPreDraw() { - int visibility = InlineContentView.this.isShown() ? VISIBLE : GONE; - mSurfaceView.setVisibility(visibility); - return true; - } - }; - /** * @inheritDoc * @hide @@ -164,6 +202,7 @@ public class InlineContentView extends ViewGroup { public InlineContentView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); + mSurfaceView.setEnableSurfaceClipping(true); } /** @@ -177,6 +216,12 @@ public class InlineContentView extends ViewGroup { return mSurfaceView.getSurfaceControl(); } + @Override + public void setClipBounds(Rect clipBounds) { + super.setClipBounds(clipBounds); + mSurfaceView.setClipBounds(clipBounds); + } + /** * @inheritDoc * @hide @@ -184,10 +229,33 @@ public class InlineContentView extends ViewGroup { public InlineContentView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - mSurfaceView = new SurfaceView(context, attrs, defStyleAttr, defStyleRes); + mSurfaceView = new SurfaceView(context, attrs, defStyleAttr, defStyleRes) { + @Override + protected void onSetSurfacePositionAndScaleRT( + @NonNull SurfaceControl.Transaction transaction, + @NonNull SurfaceControl surface, int positionLeft, int positionTop, + float postScaleX, float postScaleY) { + // If we have a parent position, we need to make our coordinates relative + // to the parent in the rendering space. + if (mParentPosition != null) { + positionLeft = (int) ((positionLeft - mParentPosition[0]) / mParentScale.x); + positionTop = (int) ((positionTop - mParentPosition[1]) / mParentScale.y); + } + + // Any scaling done to the parent or its predecessors would be applied + // via the surfaces parent -> child relation, so we only propagate any + // scaling set on the InlineContentView itself. + postScaleX = InlineContentView.this.getScaleX(); + postScaleY = InlineContentView.this.getScaleY(); + + super.onSetSurfacePositionAndScaleRT(transaction, surface, positionLeft, + positionTop, postScaleX, postScaleY); + } + }; mSurfaceView.setZOrderOnTop(true); mSurfaceView.getHolder().setFormat(PixelFormat.TRANSPARENT); addView(mSurfaceView); + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); } /** @@ -195,6 +263,7 @@ public class InlineContentView extends ViewGroup { * * @hide */ + @TestApi public void setChildSurfacePackageUpdater( @Nullable SurfacePackageUpdater surfacePackageUpdater) { mSurfacePackageUpdater = surfacePackageUpdater; @@ -213,8 +282,9 @@ public class InlineContentView extends ViewGroup { } }); } - mSurfaceView.setVisibility(VISIBLE); - getViewTreeObserver().addOnPreDrawListener(mDrawListener); + + mSurfaceView.setVisibility(getVisibility()); + getViewTreeObserver().addOnDrawListener(mOnDrawListener); } @Override @@ -224,7 +294,9 @@ public class InlineContentView extends ViewGroup { if (mSurfacePackageUpdater != null) { mSurfacePackageUpdater.onSurfacePackageReleased(); } - getViewTreeObserver().removeOnPreDrawListener(mDrawListener); + + getViewTreeObserver().removeOnDrawListener(mOnDrawListener); + mSurfaceView.setVisibility(View.GONE); } @Override @@ -271,4 +343,67 @@ public class InlineContentView extends ViewGroup { public boolean setZOrderedOnTop(boolean onTop) { return mSurfaceView.setZOrderedOnTop(onTop, /*allowDynamicChange*/ true); } + + + private void computeParentPositionAndScale() { + boolean contentPositionOrScaleChanged = false; + + // This method can be called on the UI or render thread but for the cases + // it is called these threads are not running concurrently, so no need to lock. + final SurfaceView parentSurfaceOwnerView = (mParentSurfaceOwnerView != null) + ? mParentSurfaceOwnerView.get() : null; + + if (parentSurfaceOwnerView != null) { + if (mParentPosition == null) { + mParentPosition = new int[2]; + } + final int oldParentPositionX = mParentPosition[0]; + final int oldParentPositionY = mParentPosition[1]; + parentSurfaceOwnerView.getLocationInSurface(mParentPosition); + if (oldParentPositionX != mParentPosition[0] + || oldParentPositionY != mParentPosition[1]) { + contentPositionOrScaleChanged = true; + } + + if (mParentScale == null) { + mParentScale = new PointF(); + } + + final float lastParentSurfaceWidth = parentSurfaceOwnerView + .getSurfaceRenderPosition().width(); + final float oldParentScaleX = mParentScale.x; + if (lastParentSurfaceWidth > 0) { + mParentScale.x = lastParentSurfaceWidth / + (float) parentSurfaceOwnerView.getWidth(); + } else { + mParentScale.x = 1.0f; + } + if (!contentPositionOrScaleChanged + && Float.compare(oldParentScaleX, mParentScale.x) != 0) { + contentPositionOrScaleChanged = true; + } + + final float lastParentSurfaceHeight = parentSurfaceOwnerView + .getSurfaceRenderPosition().height(); + final float oldParentScaleY = mParentScale.y; + if (lastParentSurfaceHeight > 0) { + mParentScale.y = lastParentSurfaceHeight + / (float) parentSurfaceOwnerView.getHeight(); + } else { + mParentScale.y = 1.0f; + } + if (!contentPositionOrScaleChanged + && Float.compare(oldParentScaleY, mParentScale.y) != 0) { + contentPositionOrScaleChanged = true; + } + } else if (mParentPosition != null || mParentScale != null) { + contentPositionOrScaleChanged = true; + mParentPosition = null; + mParentScale = null; + } + + if (contentPositionOrScaleChanged) { + mSurfaceView.requestUpdateSurfacePositionAndScale(); + } + } } diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index 47edc497c79f..a30c3c52f42c 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -1017,14 +1017,17 @@ public class ChooserActivity extends ResolverActivity implements /** * Update UI to reflect changes in data. - * <p>If {@code listAdapter} is {@code null}, both profile list adapters are updated. + * <p>If {@code listAdapter} is {@code null}, both profile list adapters are updated if + * available. */ private void handlePackagesChanged(@Nullable ResolverListAdapter listAdapter) { // Refresh pinned items mPinnedSharedPrefs = getPinnedSharedPrefs(this); if (listAdapter == null) { mChooserMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged(); - mChooserMultiProfilePagerAdapter.getInactiveListAdapter().handlePackagesChanged(); + if (mChooserMultiProfilePagerAdapter.getCount() > 1) { + mChooserMultiProfilePagerAdapter.getInactiveListAdapter().handlePackagesChanged(); + } } else { listAdapter.handlePackagesChanged(); } diff --git a/core/java/com/android/internal/app/ChooserListAdapter.java b/core/java/com/android/internal/app/ChooserListAdapter.java index 5efd46c4c64a..de5ab6f1c90d 100644 --- a/core/java/com/android/internal/app/ChooserListAdapter.java +++ b/core/java/com/android/internal/app/ChooserListAdapter.java @@ -561,7 +561,7 @@ public class ChooserListAdapter extends ResolverListAdapter { mChooserTargetScores.put(componentName, new HashMap<>()); } mChooserTargetScores.get(componentName).put(shortcutInfo.getShortLabel().toString(), - shortcutInfo.getRank()); + target.getRank()); } mChooserTargetScores.keySet().forEach(key -> rankTargetsWithinComponent(key)); } diff --git a/core/java/com/android/internal/app/PlatLogoActivity.java b/core/java/com/android/internal/app/PlatLogoActivity.java index 2a7eae626795..986bbc8628ec 100644 --- a/core/java/com/android/internal/app/PlatLogoActivity.java +++ b/core/java/com/android/internal/app/PlatLogoActivity.java @@ -55,6 +55,10 @@ import org.json.JSONObject; public class PlatLogoActivity extends Activity { private static final boolean WRITE_SETTINGS = true; + private static final String R_EGG_UNLOCK_SETTING = "egg_mode_r"; + + private static final int UNLOCK_TRIES = 3; + BigDialView mDialView; @Override @@ -77,8 +81,10 @@ public class PlatLogoActivity extends Activity { mDialView = new BigDialView(this, null); if (Settings.System.getLong(getContentResolver(), - "egg_mode" /* Settings.System.EGG_MODE */, 0) == 0) { - mDialView.setUnlockTries(3); + R_EGG_UNLOCK_SETTING, 0) == 0) { + mDialView.setUnlockTries(UNLOCK_TRIES); + } else { + mDialView.setUnlockTries(0); } final FrameLayout layout = new FrameLayout(this); @@ -91,18 +97,16 @@ public class PlatLogoActivity extends Activity { private void launchNextStage(boolean locked) { final ContentResolver cr = getContentResolver(); - if (Settings.System.getLong(cr, "egg_mode" /* Settings.System.EGG_MODE */, 0) == 0) { - // For posterity: the moment this user unlocked the easter egg - try { - if (WRITE_SETTINGS) { - Settings.System.putLong(cr, - "egg_mode", // Settings.System.EGG_MODE, - locked ? 0 : System.currentTimeMillis()); - } - } catch (RuntimeException e) { - Log.e("com.android.internal.app.PlatLogoActivity", "Can't write settings", e); + try { + if (WRITE_SETTINGS) { + Settings.System.putLong(cr, + R_EGG_UNLOCK_SETTING, + locked ? 0 : System.currentTimeMillis()); } + } catch (RuntimeException e) { + Log.e("com.android.internal.app.PlatLogoActivity", "Can't write settings", e); } + try { startActivity(new Intent(Intent.ACTION_MAIN) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK @@ -235,8 +239,8 @@ public class PlatLogoActivity extends Activity { } return true; case MotionEvent.ACTION_UP: - if (mWasLocked && !mDialDrawable.isLocked()) { - launchNextStage(false); + if (mWasLocked != mDialDrawable.isLocked()) { + launchNextStage(mDialDrawable.isLocked()); } return true; } @@ -404,6 +408,8 @@ public class PlatLogoActivity extends Activity { if (isLocked() && oldUserLevel != STEPS - 1 && getUserLevel() == STEPS - 1) { mUnlockTries--; + } else if (!isLocked() && getUserLevel() == 0) { + mUnlockTries = UNLOCK_TRIES; } if (!isLocked()) { diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index 86c13a0581c2..fba4675a8c9f 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -20,9 +20,6 @@ import static android.Manifest.permission.INTERACT_ACROSS_PROFILES; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.content.PermissionChecker.PID_UNKNOWN; -import static com.android.internal.app.AbstractMultiProfilePagerAdapter.PROFILE_PERSONAL; -import static com.android.internal.app.AbstractMultiProfilePagerAdapter.PROFILE_WORK; - import android.annotation.Nullable; import android.annotation.StringRes; import android.annotation.UiThread; @@ -159,6 +156,9 @@ public class ResolverActivity extends Activity implements protected static final String METRICS_CATEGORY_RESOLVER = "intent_resolver"; protected static final String METRICS_CATEGORY_CHOOSER = "intent_chooser"; + /** Tracks if we should ignore future broadcasts telling us the work profile is enabled */ + private boolean mWorkProfileHasBeenEnabled = false; + @VisibleForTesting public static boolean ENABLE_TABBED_VIEW = true; private static final String TAB_TAG_PERSONAL = "personal"; @@ -825,12 +825,23 @@ public class ResolverActivity extends Activity implements if (shouldShowTabs()) { mWorkProfileStateReceiver = createWorkProfileStateReceiver(); registerWorkProfileStateReceiver(); + + mWorkProfileHasBeenEnabled = isWorkProfileEnabled(); } } + private boolean isWorkProfileEnabled() { + UserHandle workUserHandle = getWorkProfileUserHandle(); + UserManager userManager = getSystemService(UserManager.class); + + return !userManager.isQuietModeEnabled(workUserHandle) + && userManager.isUserUnlocked(workUserHandle); + } + private void registerWorkProfileStateReceiver() { IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_USER_UNLOCKED); + filter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE); filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE); registerReceiverAsUser(mWorkProfileStateReceiver, UserHandle.ALL, filter, null, null); } @@ -1961,17 +1972,29 @@ public class ResolverActivity extends Activity implements public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (!TextUtils.equals(action, Intent.ACTION_USER_UNLOCKED) - && !TextUtils.equals(action, Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)) { + && !TextUtils.equals(action, Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE) + && !TextUtils.equals(action, Intent.ACTION_MANAGED_PROFILE_AVAILABLE)) { return; } - int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); - if (TextUtils.equals(action, Intent.ACTION_USER_UNLOCKED) - && userHandle != getWorkProfileUserHandle().getIdentifier()) { + + int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); + + if (userId != getWorkProfileUserHandle().getIdentifier()) { return; } - if (TextUtils.equals(action, Intent.ACTION_USER_UNLOCKED)) { + + if (isWorkProfileEnabled()) { + if (mWorkProfileHasBeenEnabled) { + return; + } + + mWorkProfileHasBeenEnabled = true; mMultiProfilePagerAdapter.markWorkProfileEnabledBroadcastReceived(); + } else { + // Must be an UNAVAILABLE broadcast, so we watch for the next availability + mWorkProfileHasBeenEnabled = false; } + if (mMultiProfilePagerAdapter.getCurrentUserHandle() .equals(getWorkProfileUserHandle())) { mMultiProfilePagerAdapter.rebuildActiveTab(true); diff --git a/core/java/com/android/internal/app/SuspendedAppActivity.java b/core/java/com/android/internal/app/SuspendedAppActivity.java index 0589baa76b8a..d8eaeda2b549 100644 --- a/core/java/com/android/internal/app/SuspendedAppActivity.java +++ b/core/java/com/android/internal/app/SuspendedAppActivity.java @@ -26,6 +26,7 @@ import android.Manifest; import android.annotation.Nullable; import android.app.AlertDialog; import android.app.AppGlobals; +import android.app.KeyguardManager; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentSender; @@ -208,9 +209,32 @@ public class SuspendedAppActivity extends AlertActivity ap.mPositiveButtonText = getString(android.R.string.ok); ap.mNeutralButtonText = resolveNeutralButtonText(); ap.mPositiveButtonListener = ap.mNeutralButtonListener = this; + + requestDismissKeyguardIfNeeded(ap.mMessage); + setupAlert(); } + private void requestDismissKeyguardIfNeeded(CharSequence dismissMessage) { + final KeyguardManager km = getSystemService(KeyguardManager.class); + if (km.isKeyguardLocked()) { + km.requestDismissKeyguard(this, dismissMessage, + new KeyguardManager.KeyguardDismissCallback() { + @Override + public void onDismissError() { + Slog.e(TAG, "Error while dismissing keyguard." + + " Keeping the dialog visible."); + } + + @Override + public void onDismissCancelled() { + Slog.w(TAG, "Keyguard dismiss was cancelled. Finishing."); + SuspendedAppActivity.this.finish(); + } + }); + } + } + @Override public void onClick(DialogInterface dialog, int which) { switch (which) { diff --git a/core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java b/core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java index 79397b81ace7..4b968b45f122 100644 --- a/core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java +++ b/core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java @@ -46,7 +46,8 @@ import java.lang.annotation.Retention; SoftInputShowHideReason.HIDE_SETTINGS_ON_CHANGE, SoftInputShowHideReason.HIDE_POWER_BUTTON_GO_HOME, SoftInputShowHideReason.HIDE_DOCKED_STACK_ATTACHED, - SoftInputShowHideReason.HIDE_RECENTS_ANIMATION}) + SoftInputShowHideReason.HIDE_RECENTS_ANIMATION, + SoftInputShowHideReason.HIDE_BUBBLES}) public @interface SoftInputShowHideReason { /** Show soft input by {@link android.view.inputmethod.InputMethodManager#showSoftInput}. */ int SHOW_SOFT_INPUT = 0; @@ -140,4 +141,10 @@ public @interface SoftInputShowHideReason { * intercept touch from app window. */ int HIDE_RECENTS_ANIMATION = 18; + + /** + * Hide soft input when {@link com.android.systemui.bubbles.BubbleController} is expanding, + * switching, or collapsing Bubbles. + */ + int HIDE_BUBBLES = 19; } diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl index c32082418bc5..4999ec055608 100644 --- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl @@ -79,6 +79,7 @@ interface IStatusBarService void onNotificationSettingsViewed(String key); void onNotificationBubbleChanged(String key, boolean isBubble, int flags); void onBubbleNotificationSuppressionChanged(String key, boolean isSuppressed); + void hideCurrentInputMethodForBubbles(); void grantInlineReplyUriPermission(String key, in Uri uri, in UserHandle user, String packageName); void clearInlineReplyUriPermissions(String key); diff --git a/core/java/com/android/internal/widget/ConversationLayout.java b/core/java/com/android/internal/widget/ConversationLayout.java index 3fcedd9156a6..0791ed3c42ec 100644 --- a/core/java/com/android/internal/widget/ConversationLayout.java +++ b/core/java/com/android/internal/widget/ConversationLayout.java @@ -1304,8 +1304,10 @@ public class ConversationLayout extends FrameLayout if (expandable) { mExpandButtonContainer.setVisibility(VISIBLE); mExpandButtonInnerContainer.setOnClickListener(onClickListener); + mConversationIconContainer.setOnClickListener(onClickListener); } else { mExpandButtonContainer.setVisibility(GONE); + mConversationIconContainer.setOnClickListener(null); } updateContentEndPaddings(); } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 9c1ecf2e48e0..9945057f0e94 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -4850,7 +4850,7 @@ <!-- @SystemApi Allows an application to turn on / off quiet mode. @hide --> <permission android:name="android.permission.MODIFY_QUIET_MODE" - android:protectionLevel="signature|privileged|wellbeing" /> + android:protectionLevel="signature|privileged|wellbeing|development" /> <!-- Allows internal management of the camera framework @hide --> diff --git a/core/res/res/layout/notification_template_material_conversation.xml b/core/res/res/layout/notification_template_material_conversation.xml index 139185f98b69..861a056f3569 100644 --- a/core/res/res/layout/notification_template_material_conversation.xml +++ b/core/res/res/layout/notification_template_material_conversation.xml @@ -128,6 +128,9 @@ android:layout_weight="1"> <!-- Header --> + + <!-- Use layout_marginStart instead of paddingStart to work around strange + measurement behavior on lower display densities. --> <LinearLayout android:id="@+id/conversation_header" android:layout_width="wrap_content" @@ -135,7 +138,7 @@ android:orientation="horizontal" android:paddingTop="16dp" android:layout_marginBottom="2dp" - android:paddingStart="@dimen/conversation_content_start" + android:layout_marginStart="@dimen/conversation_content_start" > <TextView android:id="@+id/conversation_text" diff --git a/core/tests/bugreports/src/android/server/bugreports/BugreportManagerTest.java b/core/tests/bugreports/src/android/server/bugreports/BugreportManagerTest.java index c72707db9560..153337727e96 100644 --- a/core/tests/bugreports/src/android/server/bugreports/BugreportManagerTest.java +++ b/core/tests/bugreports/src/android/server/bugreports/BugreportManagerTest.java @@ -58,6 +58,8 @@ public class BugreportManagerTest { private Handler mHandler; private Executor mExecutor; private BugreportManager mBrm; + private File mBugreportFile; + private File mScreenshotFile; private ParcelFileDescriptor mBugreportFd; private ParcelFileDescriptor mScreenshotFd; @@ -73,8 +75,10 @@ public class BugreportManagerTest { }; mBrm = getBugreportManager(); - mBugreportFd = parcelFd("bugreport_" + name.getMethodName(), ".zip"); - mScreenshotFd = parcelFd("screenshot_" + name.getMethodName(), ".png"); + mBugreportFile = createTempFile("bugreport_" + name.getMethodName(), ".zip"); + mScreenshotFile = createTempFile("screenshot_" + name.getMethodName(), ".png"); + mBugreportFd = parcelFd(mBugreportFile); + mScreenshotFd = parcelFd(mScreenshotFile); getPermissions(); } @@ -121,6 +125,21 @@ public class BugreportManagerTest { } @Test + public void normalFlow_full() throws Exception { + BugreportCallbackImpl callback = new BugreportCallbackImpl(); + mBrm.startBugreport(mBugreportFd, mScreenshotFd, full(), mExecutor, callback); + + waitTillDoneOrTimeout(callback); + assertThat(callback.isDone()).isTrue(); + assertThat(callback.getErrorCode()).isEqualTo( + BugreportCallback.BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT); + // bugreport and screenshot files should be empty when user consent timed out. + assertThat(mBugreportFile.length()).isEqualTo(0); + assertThat(mScreenshotFile.length()).isEqualTo(0); + assertFdsAreClosed(mBugreportFd, mScreenshotFd); + } + + @Test public void simultaneousBugreportsNotAllowed() throws Exception { // Start bugreport #1 BugreportCallbackImpl callback = new BugreportCallbackImpl(); @@ -129,9 +148,10 @@ public class BugreportManagerTest { // Before #1 is done, try to start #2. assertThat(callback.isDone()).isFalse(); BugreportCallbackImpl callback2 = new BugreportCallbackImpl(); - ParcelFileDescriptor bugreportFd2 = parcelFd("bugreport_2_" + name.getMethodName(), ".zip"); - ParcelFileDescriptor screenshotFd2 = - parcelFd("screenshot_2_" + name.getMethodName(), ".png"); + File bugreportFile2 = createTempFile("bugreport_2_" + name.getMethodName(), ".zip"); + File screenshotFile2 = createTempFile("screenshot_2_" + name.getMethodName(), ".png"); + ParcelFileDescriptor bugreportFd2 = parcelFd(bugreportFile2); + ParcelFileDescriptor screenshotFd2 = parcelFd(screenshotFile2); mBrm.startBugreport(bugreportFd2, screenshotFd2, wifi(), mExecutor, callback2); Thread.sleep(500 /* .5s */); @@ -271,12 +291,16 @@ public class BugreportManagerTest { return bm; } - private static ParcelFileDescriptor parcelFd(String prefix, String extension) throws Exception { - File f = File.createTempFile(prefix, extension); + private static File createTempFile(String prefix, String extension) throws Exception { + final File f = File.createTempFile(prefix, extension); f.setReadable(true, true); f.setWritable(true, true); + f.deleteOnExit(); + return f; + } - return ParcelFileDescriptor.open(f, + private static ParcelFileDescriptor parcelFd(File file) throws Exception { + return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_APPEND); } @@ -342,4 +366,13 @@ public class BugreportManagerTest { private static BugreportParams interactive() { return new BugreportParams(BugreportParams.BUGREPORT_MODE_INTERACTIVE); } + + /* + * Returns a {@link BugreportParams} for full bugreport that includes a screenshot. + * + * <p> This can take on the order of minutes to finish + */ + private static BugreportParams full() { + return new BugreportParams(BugreportParams.BUGREPORT_MODE_FULL); + } } diff --git a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java index bf7f339a8484..1b3272572db0 100644 --- a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java +++ b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java @@ -26,13 +26,11 @@ import static junit.framework.Assert.assertEquals; import static junit.framework.TestCase.assertFalse; import static junit.framework.TestCase.assertTrue; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; -import static org.mockito.Mockito.when; import android.app.Instrumentation; import android.content.Context; @@ -135,37 +133,29 @@ public class InsetsSourceConsumerTest { InsetsSourceConsumer consumer = new InsetsSourceConsumer( ITYPE_IME, state, null, controller); - when(controller.getAnimationType(anyInt())).thenReturn(ANIMATION_TYPE_NONE); - InsetsSource source = new InsetsSource(ITYPE_IME); source.setFrame(0, 1, 2, 3); - consumer.updateSource(new InsetsSource(source)); - - when(controller.getAnimationType(anyInt())).thenReturn(ANIMATION_TYPE_USER); + consumer.updateSource(new InsetsSource(source), ANIMATION_TYPE_NONE); // While we're animating, updates are delayed source.setFrame(4, 5, 6, 7); - consumer.updateSource(new InsetsSource(source)); + consumer.updateSource(new InsetsSource(source), ANIMATION_TYPE_USER); assertEquals(new Rect(0, 1, 2, 3), state.peekSource(ITYPE_IME).getFrame()); // Finish the animation, now the pending frame should be applied - when(controller.getAnimationType(anyInt())).thenReturn(ANIMATION_TYPE_NONE); assertTrue(consumer.notifyAnimationFinished()); assertEquals(new Rect(4, 5, 6, 7), state.peekSource(ITYPE_IME).getFrame()); - when(controller.getAnimationType(anyInt())).thenReturn(ANIMATION_TYPE_USER); - // Animating again, updates are delayed source.setFrame(8, 9, 10, 11); - consumer.updateSource(new InsetsSource(source)); + consumer.updateSource(new InsetsSource(source), ANIMATION_TYPE_USER); assertEquals(new Rect(4, 5, 6, 7), state.peekSource(ITYPE_IME).getFrame()); // Updating with the current frame triggers a different code path, verify this clears // the pending 8, 9, 10, 11 frame: source.setFrame(4, 5, 6, 7); - consumer.updateSource(new InsetsSource(source)); + consumer.updateSource(new InsetsSource(source), ANIMATION_TYPE_USER); - when(controller.getAnimationType(anyInt())).thenReturn(ANIMATION_TYPE_NONE); assertFalse(consumer.notifyAnimationFinished()); assertEquals(new Rect(4, 5, 6, 7), state.peekSource(ITYPE_IME).getFrame()); } diff --git a/core/tests/coretests/src/android/view/InsetsStateTest.java b/core/tests/coretests/src/android/view/InsetsStateTest.java index 7115acfedcf6..5260ef83cc4f 100644 --- a/core/tests/coretests/src/android/view/InsetsStateTest.java +++ b/core/tests/coretests/src/android/view/InsetsStateTest.java @@ -304,6 +304,7 @@ public class InsetsStateTest { mState.getSource(ITYPE_IME).setVisibleFrame(new Rect(0, 0, 50, 10)); mState.getSource(ITYPE_IME).setVisible(true); mState.getSource(ITYPE_STATUS_BAR).setFrame(new Rect(0, 0, 100, 100)); + mState2.getSource(ITYPE_NAVIGATION_BAR).setFrame(new Rect(0, 0, 100, 100)); mState2.set(mState, true); assertEquals(mState, mState2); } diff --git a/identity/java/android/security/identity/IdentityCredential.java b/identity/java/android/security/identity/IdentityCredential.java index 493c85a930be..4eb6e420c07f 100644 --- a/identity/java/android/security/identity/IdentityCredential.java +++ b/identity/java/android/security/identity/IdentityCredential.java @@ -41,19 +41,18 @@ public abstract class IdentityCredential { /** * Create an ephemeral key pair to use to establish a secure channel with a reader. * - * <p>Most applications will use only the public key, and only to send it to the reader, - * allowing the private key to be used internally for {@link #encryptMessageToReader(byte[])} - * and {@link #decryptMessageFromReader(byte[])}. The private key is also provided for - * applications that wish to use a cipher suite that is not supported by - * {@link IdentityCredentialStore}. + * <p>Applications should use this key-pair for the communications channel with the reader + * using a protocol / cipher-suite appropriate for the application. One example of such a + * protocol is the one used for Mobile Driving Licenses, see ISO 18013-5 section 9.2.1 "Session + * encryption". * * @return ephemeral key pair to use to establish a secure channel with a reader. */ public @NonNull abstract KeyPair createEphemeralKeyPair(); /** - * Set the ephemeral public key provided by the reader. This must be called before - * {@link #encryptMessageToReader} or {@link #decryptMessageFromReader} can be called. + * Set the ephemeral public key provided by the reader. If called, this must be called before + * {@link #getEntries(byte[], Map, byte[], byte[])} is called. * * @param readerEphemeralPublicKey The ephemeral public key provided by the reader to * establish a secure session. @@ -65,6 +64,11 @@ public abstract class IdentityCredential { /** * Encrypt a message for transmission to the reader. * + * <p>Do not use. In this version of the API, this method produces an incorrect + * result. Instead, applications should implement message encryption/decryption themselves as + * detailed in the {@link #createEphemeralKeyPair()} method. In a future API-level, this + * method will be deprecated. + * * @param messagePlaintext unencrypted message to encrypt. * @return encrypted message. */ @@ -73,6 +77,11 @@ public abstract class IdentityCredential { /** * Decrypt a message received from the reader. * + * <p>Do not use. In this version of the API, this method produces an incorrect + * result. Instead, applications should implement message encryption/decryption themselves as + * detailed in the {@link #createEphemeralKeyPair()} method. In a future API-level, this + * method will be deprecated. + * * @param messageCiphertext encrypted message to decrypt. * @return decrypted message. * @throws MessageDecryptionException if the ciphertext couldn't be decrypted. @@ -178,7 +187,7 @@ public abstract class IdentityCredential { * * <p>If {@code readerAuth} is not {@code null} it must be the bytes of a {@code COSE_Sign1} * structure as defined in RFC 8152. For the payload nil shall be used and the - * detached payload is the ReaderAuthentication CBOR described below. + * detached payload is the ReaderAuthenticationBytes CBOR described below. * <pre> * ReaderAuthentication = [ * "ReaderAuthentication", @@ -186,7 +195,9 @@ public abstract class IdentityCredential { * ItemsRequestBytes * ] * - * ItemsRequestBytes = #6.24(bstr .cbor ItemsRequest) ; Bytes of ItemsRequest + * ItemsRequestBytes = #6.24(bstr .cbor ItemsRequest) + * + * ReaderAuthenticationBytes = #6.24(bstr .cbor ReaderAuthentication) * </pre> * * <p>where {@code ItemsRequestBytes} are the bytes in the {@code requestMessage} parameter. diff --git a/identity/java/android/security/identity/ResultData.java b/identity/java/android/security/identity/ResultData.java index 37de2c4a50ea..71860d261285 100644 --- a/identity/java/android/security/identity/ResultData.java +++ b/identity/java/android/security/identity/ResultData.java @@ -68,8 +68,8 @@ public abstract class ResultData { * {@link #getMessageAuthenticationCode()} can be used to get a MAC. * * <p>The CBOR structure which is cryptographically authenticated is the - * {@code DeviceAuthentication} structure according to the following - * <a href="https://tools.ietf.org/html/draft-ietf-cbor-cddl-06">CDDL</a> schema: + * {@code DeviceAuthenticationBytes} structure according to the following + * <a href="https://tools.ietf.org/html/rfc8610">CDDL</a> schema: * * <pre> * DeviceAuthentication = [ @@ -80,15 +80,9 @@ public abstract class ResultData { * ] * * DocType = tstr - * - * SessionTranscript = [ - * DeviceEngagementBytes, - * EReaderKeyBytes - * ] - * - * DeviceEngagementBytes = #6.24(bstr .cbor DeviceEngagement) - * EReaderKeyBytes = #6.24(bstr .cbor EReaderKey.Pub) + * SessionTranscript = any * DeviceNameSpacesBytes = #6.24(bstr .cbor DeviceNameSpaces) + * DeviceAuthenticationBytes = #6.24(bstr .cbor DeviceAuthentication) * </pre> * * <p>where @@ -115,7 +109,7 @@ public abstract class ResultData { public abstract @NonNull byte[] getAuthenticatedData(); /** - * Returns a message authentication code over the {@code DeviceAuthentication} CBOR + * Returns a message authentication code over the {@code DeviceAuthenticationBytes} CBOR * specified in {@link #getAuthenticatedData()}, to prove to the reader that the data * is from a trusted credential. * diff --git a/location/java/android/location/AbstractListenerManager.java b/location/java/android/location/AbstractListenerManager.java index 36b86899f2d8..f5595e89b807 100644 --- a/location/java/android/location/AbstractListenerManager.java +++ b/location/java/android/location/AbstractListenerManager.java @@ -29,6 +29,8 @@ import android.util.ArrayMap; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.Preconditions; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -193,7 +195,7 @@ abstract class AbstractListenerManager<TRequest, TListener> { protected abstract void unregisterService() throws RemoteException; @Nullable - protected TRequest merge(@NonNull TRequest[] requests) { + protected TRequest merge(@NonNull List<TRequest> requests) { for (TRequest request : requests) { Preconditions.checkArgument(request == null, "merge() has to be overridden for non-null requests."); @@ -221,9 +223,9 @@ abstract class AbstractListenerManager<TRequest, TListener> { return mListeners.valueAt(0).getRequest(); } - TRequest[] requests = (TRequest[]) new Object[mListeners.size()]; + ArrayList<TRequest> requests = new ArrayList<>(mListeners.size()); for (int index = 0; index < mListeners.size(); index++) { - requests[index] = mListeners.valueAt(index).getRequest(); + requests.add(mListeners.valueAt(index).getRequest()); } return merge(requests); } diff --git a/location/java/android/location/LocationManager.java b/location/java/android/location/LocationManager.java index 7d15bbd46697..f3c9e9435417 100644 --- a/location/java/android/location/LocationManager.java +++ b/location/java/android/location/LocationManager.java @@ -3030,14 +3030,14 @@ public class LocationManager { @Override @Nullable - protected GnssRequest merge(@NonNull GnssRequest[] requests) { - Preconditions.checkArgument(requests.length > 0); + protected GnssRequest merge(@NonNull List<GnssRequest> requests) { + Preconditions.checkArgument(!requests.isEmpty()); for (GnssRequest request : requests) { if (request.isFullTracking()) { return request; } } - return requests[0]; + return requests.get(0); } private class GnssMeasurementsListener extends IGnssMeasurementsListener.Stub { diff --git a/media/java/android/media/IMediaRouter2.aidl b/media/java/android/media/IMediaRouter2.aidl index ca14052c964f..fe15f0e67b1d 100644 --- a/media/java/android/media/IMediaRouter2.aidl +++ b/media/java/android/media/IMediaRouter2.aidl @@ -34,7 +34,8 @@ oneway interface IMediaRouter2 { void notifySessionReleased(in RoutingSessionInfo sessionInfo); /** * Gets hints of the new session for the given route. - * Call MediaRouterService#notifySessionHintsForCreatingSession to pass the result. + * Call MediaRouterService#requestCreateSessionWithRouter2 to pass the result. */ - void getSessionHintsForCreatingSession(long uniqueRequestId, in MediaRoute2Info route); + void requestCreateSessionByManager(long uniqueRequestId, in RoutingSessionInfo oldSession, + in MediaRoute2Info route); } diff --git a/media/java/android/media/IMediaRouterService.aidl b/media/java/android/media/IMediaRouterService.aidl index 52bac671cc6f..068f9689d06f 100644 --- a/media/java/android/media/IMediaRouterService.aidl +++ b/media/java/android/media/IMediaRouterService.aidl @@ -44,7 +44,7 @@ interface IMediaRouterService { void requestSetVolume(IMediaRouterClient client, String routeId, int volume); void requestUpdateVolume(IMediaRouterClient client, String routeId, int direction); - // Note: When changing this file, match the order of methods below with + // Note: When changing this file, match the order of methods below with // MediaRouterService.java for readability. // Methods for MediaRouter2 @@ -57,10 +57,9 @@ interface IMediaRouterService { in RouteDiscoveryPreference preference); void setRouteVolumeWithRouter2(IMediaRouter2 router, in MediaRoute2Info route, int volume); - void requestCreateSessionWithRouter2(IMediaRouter2 router, int requestId, - in MediaRoute2Info route, in @nullable Bundle sessionHints); - void notifySessionHintsForCreatingSession(IMediaRouter2 router, long uniqueRequestId, - in MediaRoute2Info route, in @nullable Bundle sessionHints); + void requestCreateSessionWithRouter2(IMediaRouter2 router, int requestId, long managerRequestId, + in RoutingSessionInfo oldSession, in MediaRoute2Info route, + in @nullable Bundle sessionHints); void selectRouteWithRouter2(IMediaRouter2 router, String sessionId, in MediaRoute2Info route); void deselectRouteWithRouter2(IMediaRouter2 router, String sessionId, in MediaRoute2Info route); void transferToRouteWithRouter2(IMediaRouter2 router, String sessionId, @@ -76,7 +75,7 @@ interface IMediaRouterService { in MediaRoute2Info route, int volume); void requestCreateSessionWithManager(IMediaRouter2Manager manager, int requestId, - String packageName, in @nullable MediaRoute2Info route); + in RoutingSessionInfo oldSession, in @nullable MediaRoute2Info route); void selectRouteWithManager(IMediaRouter2Manager manager, int requestId, String sessionId, in MediaRoute2Info route); void deselectRouteWithManager(IMediaRouter2Manager manager, int requestId, diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java index 62d76c0d2d58..0780c6875eb5 100644 --- a/media/java/android/media/MediaCodec.java +++ b/media/java/android/media/MediaCodec.java @@ -2762,7 +2762,7 @@ final public class MediaCodec { builder.append(hexdigits.charAt(key[i] & 0x0f)); } builder.append("], iv ["); - for (int i = 0; i < key.length; i++) { + for (int i = 0; i < iv.length; i++) { builder.append(hexdigits.charAt((iv[i] & 0xf0) >> 4)); builder.append(hexdigits.charAt(iv[i] & 0x0f)); } diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index 8e95239a73f8..f22222d10ad8 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -36,7 +36,6 @@ import com.android.internal.annotations.GuardedBy; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -62,6 +61,11 @@ public final class MediaRouter2 { private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final Object sRouterLock = new Object(); + // The maximum time for the old routing controller available after transfer. + private static final int TRANSFER_TIMEOUT_MS = 30_000; + // The manager request ID representing that no manager is involved. + private static final long MANAGER_REQUEST_ID_NONE = MediaRoute2ProviderService.REQUEST_ID_NONE; + @GuardedBy("sRouterLock") private static MediaRouter2 sInstance; @@ -80,7 +84,7 @@ public final class MediaRouter2 { private final String mPackageName; @GuardedBy("sRouterLock") - final Map<String, MediaRoute2Info> mRoutes = new HashMap<>(); + final Map<String, MediaRoute2Info> mRoutes = new ArrayMap<>(); final RoutingController mSystemController; @@ -94,7 +98,7 @@ public final class MediaRouter2 { @GuardedBy("sRouterLock") private final Map<String, RoutingController> mNonSystemRoutingControllers = new ArrayMap<>(); - private final AtomicInteger mControllerCreationRequestCnt = new AtomicInteger(1); + private final AtomicInteger mNextRequestId = new AtomicInteger(1); final Handler mHandler; @GuardedBy("sRouterLock") @@ -412,9 +416,16 @@ public final class MediaRouter2 { return; } - final int requestId = mControllerCreationRequestCnt.getAndIncrement(); + requestCreateController(controller, route, MANAGER_REQUEST_ID_NONE); + } + + void requestCreateController(@NonNull RoutingController controller, + @NonNull MediaRoute2Info route, long managerRequestId) { - ControllerCreationRequest request = new ControllerCreationRequest(requestId, route); + final int requestId = mNextRequestId.getAndIncrement(); + + ControllerCreationRequest request = new ControllerCreationRequest(requestId, + managerRequestId, route, controller); mControllerCreationRequests.add(request); OnGetControllerHintsListener listener = mOnGetControllerHintsListener; @@ -433,11 +444,15 @@ public final class MediaRouter2 { if (stub != null) { try { mMediaRouterService.requestCreateSessionWithRouter2( - stub, requestId, route, controllerHints); + stub, requestId, managerRequestId, + controller.getRoutingSessionInfo(), route, controllerHints); } catch (RemoteException ex) { - Log.e(TAG, "transfer: Unable to request to create controller.", ex); - mHandler.sendMessage(obtainMessage(MediaRouter2::createControllerOnHandler, - MediaRouter2.this, requestId, null)); + Log.e(TAG, "createControllerForTransfer: " + + "Failed to request for creating a controller.", ex); + mControllerCreationRequests.remove(request); + if (managerRequestId == MANAGER_REQUEST_ID_NONE) { + notifyTransferFailure(route); + } } } } @@ -463,7 +478,8 @@ public final class MediaRouter2 { } /** - * Gets the list of currently non-released {@link RoutingController routing controllers}. + * Gets the list of currently active {@link RoutingController routing controllers} on which + * media can be played. * <p> * Note: The list returned here will never be empty. The first element in the list is * always the {@link #getSystemController() system controller}. @@ -554,13 +570,13 @@ public final class MediaRouter2 { mShouldUpdateRoutes = true; } - if (addedRoutes.size() > 0) { + if (!addedRoutes.isEmpty()) { notifyRoutesAdded(addedRoutes); } - if (removedRoutes.size() > 0) { + if (!removedRoutes.isEmpty()) { notifyRoutesRemoved(removedRoutes); } - if (changedRoutes.size() > 0) { + if (!changedRoutes.isEmpty()) { notifyRoutesChanged(changedRoutes); } @@ -582,7 +598,7 @@ public final class MediaRouter2 { } mShouldUpdateRoutes = true; } - if (addedRoutes.size() > 0) { + if (!addedRoutes.isEmpty()) { notifyRoutesAdded(addedRoutes); } } @@ -598,7 +614,7 @@ public final class MediaRouter2 { } mShouldUpdateRoutes = true; } - if (removedRoutes.size() > 0) { + if (!removedRoutes.isEmpty()) { notifyRoutesRemoved(removedRoutes); } } @@ -614,7 +630,7 @@ public final class MediaRouter2 { } mShouldUpdateRoutes = true; } - if (changedRoutes.size() > 0) { + if (!changedRoutes.isEmpty()) { notifyRoutesChanged(changedRoutes); } } @@ -635,44 +651,47 @@ public final class MediaRouter2 { } } - if (matchingRequest != null) { - mControllerCreationRequests.remove(matchingRequest); - - MediaRoute2Info requestedRoute = matchingRequest.mRoute; - - if (sessionInfo == null) { - // TODO: We may need to distinguish between failure and rejection. - // One way can be introducing 'reason'. - notifyTransferFailure(requestedRoute); - return; - } else if (!sessionInfo.getSelectedRoutes().contains(requestedRoute.getId())) { - Log.w(TAG, "The session does not contain the requested route. " - + "(requestedRouteId=" + requestedRoute.getId() - + ", actualRoutes=" + sessionInfo.getSelectedRoutes() - + ")"); - notifyTransferFailure(requestedRoute); - return; - } else if (!TextUtils.equals(requestedRoute.getProviderId(), - sessionInfo.getProviderId())) { - Log.w(TAG, "The session's provider ID does not match the requested route's. " - + "(requested route's providerId=" + requestedRoute.getProviderId() - + ", actual providerId=" + sessionInfo.getProviderId() - + ")"); - notifyTransferFailure(requestedRoute); - return; - } + if (matchingRequest == null) { + Log.w(TAG, "createControllerOnHandler: Ignoring an unknown request."); + return; } + mControllerCreationRequests.remove(matchingRequest); + MediaRoute2Info requestedRoute = matchingRequest.mRoute; + + // TODO: Notify the reason for failure. if (sessionInfo == null) { + notifyTransferFailure(requestedRoute); + return; + } else if (!sessionInfo.getSelectedRoutes().contains(requestedRoute.getId())) { + Log.w(TAG, "The session does not contain the requested route. " + + "(requestedRouteId=" + requestedRoute.getId() + + ", actualRoutes=" + sessionInfo.getSelectedRoutes() + + ")"); + notifyTransferFailure(requestedRoute); + return; + } else if (!TextUtils.equals(requestedRoute.getProviderId(), + sessionInfo.getProviderId())) { + Log.w(TAG, "The session's provider ID does not match the requested route's. " + + "(requested route's providerId=" + requestedRoute.getProviderId() + + ", actual providerId=" + sessionInfo.getProviderId() + + ")"); + notifyTransferFailure(requestedRoute); return; } - RoutingController oldController = getCurrentController(); - if (!oldController.releaseInternal( - /* shouldReleaseSession= */ matchingRequest != null, - /* shouldNotifyStop= */ false)) { - // Could not release the controller since it was just released by other thread. - oldController = getSystemController(); + RoutingController oldController = matchingRequest.mOldController; + // When the old controller is released before transferred, treat it as a failure. + // This could also happen when transfer is requested twice or more. + if (!oldController.scheduleRelease()) { + Log.w(TAG, "createControllerOnHandler: " + + "Ignoring controller creation for released old controller. " + + "oldController=" + oldController); + if (!sessionInfo.isSystemSession()) { + new RoutingController(sessionInfo).release(); + } + notifyTransferFailure(requestedRoute); + return; } RoutingController newController; @@ -686,12 +705,7 @@ public final class MediaRouter2 { } } - // Two controller can be same if stop() is called before the result of Cast -> Phone comes. - if (oldController != newController) { - notifyTransfer(oldController, newController); - } else if (matchingRequest != null) { - notifyTransferFailure(matchingRequest.mRoute); - } + notifyTransfer(oldController, newController); } void updateControllerOnHandler(RoutingSessionInfo sessionInfo) { @@ -736,10 +750,9 @@ public final class MediaRouter2 { return; } - final String uniqueSessionId = sessionInfo.getId(); RoutingController matchingController; synchronized (sRouterLock) { - matchingController = mNonSystemRoutingControllers.get(uniqueSessionId); + matchingController = mNonSystemRoutingControllers.get(sessionInfo.getId()); } if (matchingController == null) { @@ -757,34 +770,23 @@ public final class MediaRouter2 { return; } - matchingController.releaseInternal( - /* shouldReleaseSession= */ false, /* shouldNotifyStop= */ true); + matchingController.releaseInternal(/* shouldReleaseSession= */ false); } - void onGetControllerHintsForCreatingSessionOnHandler(long uniqueRequestId, - MediaRoute2Info route) { - OnGetControllerHintsListener listener = mOnGetControllerHintsListener; - Bundle controllerHints = null; - if (listener != null) { - controllerHints = listener.onGetControllerHints(route); - if (controllerHints != null) { - controllerHints = new Bundle(controllerHints); + void onRequestCreateControllerByManagerOnHandler(RoutingSessionInfo oldSession, + MediaRoute2Info route, long managerRequestId) { + RoutingController controller; + if (oldSession.isSystemSession()) { + controller = getSystemController(); + } else { + synchronized (sRouterLock) { + controller = mNonSystemRoutingControllers.get(oldSession.getId()); } } - - MediaRouter2Stub stub; - synchronized (sRouterLock) { - stub = mStub; - } - if (stub != null) { - try { - mMediaRouterService.notifySessionHintsForCreatingSession( - stub, uniqueRequestId, route, controllerHints); - } catch (RemoteException ex) { - Log.e(TAG, "onGetControllerHintsForCreatingSessionOnHandler: Unable to notify " - + " session hints for creating session.", ex); - } + if (controller == null) { + return; } + requestCreateController(controller, route, managerRequestId); } private List<MediaRoute2Info> filterRoutes(List<MediaRoute2Info> routes, @@ -886,8 +888,13 @@ public final class MediaRouter2 { /** * Called when a media is transferred between two different routing controllers. * This can happen by calling {@link #transferTo(MediaRoute2Info)}. - * The {@code oldController} is released before this method is called, except for the - * {@link #getSystemController() system controller}. + * <p> Override this to start playback with {@code newController}. You may want to get + * the status of the media that is being played with {@code oldController} and resume it + * continuously with {@code newController}. + * After this is called, any callbacks with {@code oldController} will not be invoked + * unless {@code oldController} is the {@link #getSystemController() system controller}. + * You need to {@link RoutingController#release() release} {@code oldController} before + * playing the media with {@code newController}. * * @param oldController the previous controller that controlled routing * @param newController the new controller to control routing @@ -906,16 +913,15 @@ public final class MediaRouter2 { /** * Called when a media routing stops. It can be stopped by a user or a provider. * App should not continue playing media locally when this method is called. - * The {@code oldController} is released before this method is called, except for the - * {@link #getSystemController() system controller}. + * The {@code controller} is released before this method is called. * - * @param controller the controller that controlled the stopped media routing. + * @param controller the controller that controlled the stopped media routing */ public void onStop(@NonNull RoutingController controller) { } } /** - * A listener interface to send an optional app-specific hints when creating the + * A listener interface to send optional app-specific hints when creating a * {@link RoutingController}. */ public interface OnGetControllerHintsListener { @@ -929,9 +935,9 @@ public final class MediaRouter2 { * The method will be called on the same thread that calls * {@link #transferTo(MediaRoute2Info)} or the main thread if it is requested by the system. * - * @param route The route to create controller with + * @param route the route to create a controller with * @return An optional bundle of app-specific arguments to send to the provider, - * or null if none. The contents of this bundle may affect the result of + * or {@code null} if none. The contents of this bundle may affect the result of * controller creation. * @see MediaRoute2ProviderService#onCreateSession(long, String, String, Bundle) */ @@ -944,10 +950,11 @@ public final class MediaRouter2 { */ public abstract static class ControllerCallback { /** - * Called when a controller is updated. (e.g., the selected routes of the - * controller is changed or the volume of the controller is changed.) + * Called when a controller is updated. (e.g., when the selected routes of the + * controller is changed or when the volume of the controller is changed.) * - * @param controller the updated controller. Can be the system controller. + * @param controller the updated controller. It may be the + * {@link #getSystemController() system controller}. * @see #getSystemController() */ public void onControllerUpdated(@NonNull RoutingController controller) { } @@ -955,20 +962,28 @@ public final class MediaRouter2 { /** * A class to control media routing session in media route provider. - * For example, selecting/deselecting/transferring routes to a session can be done through this - * class. Instances are created by {@link #transferTo(MediaRoute2Info)}. + * For example, selecting/deselecting/transferring to routes of a session can be done through + * this. Instances are created when + * {@link TransferCallback#onTransfer(RoutingController, RoutingController)} is called, + * which is invoked after {@link #transferTo(MediaRoute2Info)} is called. */ public class RoutingController { private final Object mControllerLock = new Object(); + private static final int CONTROLLER_STATE_UNKNOWN = 0; + private static final int CONTROLLER_STATE_ACTIVE = 1; + private static final int CONTROLLER_STATE_RELEASING = 2; + private static final int CONTROLLER_STATE_RELEASED = 3; + @GuardedBy("mControllerLock") private RoutingSessionInfo mSessionInfo; @GuardedBy("mControllerLock") - private volatile boolean mIsReleased; + private int mState; RoutingController(@NonNull RoutingSessionInfo sessionInfo) { mSessionInfo = sessionInfo; + mState = CONTROLLER_STATE_ACTIVE; } /** @@ -982,7 +997,7 @@ public final class MediaRouter2 { } /** - * Gets the original session id set by + * Gets the original session ID set by * {@link RoutingSessionInfo.Builder#Builder(String, String)}. * * @hide @@ -996,7 +1011,8 @@ public final class MediaRouter2 { } /** - * @return the control hints used to control routing session if available. + * Gets the control hints used to control routing session if available. + * It is set by the media route provider. */ @Nullable public Bundle getControlHints() { @@ -1042,7 +1058,9 @@ public final class MediaRouter2 { } /** - * Gets information about how volume is handled on the session. + * Gets the information about how volume is handled on the session. + * <p>Please note that you may not control the volume of the session even when + * you can control the volume of each selected route in the session. * * @return {@link MediaRoute2Info#PLAYBACK_VOLUME_FIXED} or * {@link MediaRoute2Info#PLAYBACK_VOLUME_VARIABLE} @@ -1067,8 +1085,8 @@ public final class MediaRouter2 { * Gets the current volume of the session. * <p> * When it's available, it represents the volume of routing session, which is a group - * of selected routes. To get the volume of a route, - * use {@link MediaRoute2Info#getVolume()}. + * of selected routes. Use {@link MediaRoute2Info#getVolume()} + * to get the volume of a route, * </p> * @see MediaRoute2Info#getVolume() */ @@ -1087,7 +1105,7 @@ public final class MediaRouter2 { */ public boolean isReleased() { synchronized (mControllerLock) { - return mIsReleased; + return mState == CONTROLLER_STATE_RELEASED; } } @@ -1099,8 +1117,8 @@ public final class MediaRouter2 { * <p> * The given route must satisfy all of the following conditions: * <ul> - * <li>ID should not be included in {@link #getSelectedRoutes()}</li> - * <li>ID should be included in {@link #getSelectableRoutes()}</li> + * <li>It should not be included in {@link #getSelectedRoutes()}</li> + * <li>It should be included in {@link #getSelectableRoutes()}</li> * </ul> * If the route doesn't meet any of above conditions, it will be ignored. * @@ -1111,11 +1129,9 @@ public final class MediaRouter2 { */ public void selectRoute(@NonNull MediaRoute2Info route) { Objects.requireNonNull(route, "route must not be null"); - synchronized (mControllerLock) { - if (mIsReleased) { - Log.w(TAG, "selectRoute: Called on released controller. Ignoring."); - return; - } + if (isReleased()) { + Log.w(TAG, "selectRoute: Called on released controller. Ignoring."); + return; } List<MediaRoute2Info> selectedRoutes = getSelectedRoutes(); @@ -1145,12 +1161,12 @@ public final class MediaRouter2 { /** * Deselects a route from the remote session. After a route is deselected, the media is - * expected to be stopped on the deselected routes. + * expected to be stopped on the deselected route. * <p> * The given route must satisfy all of the following conditions: * <ul> - * <li>ID should be included in {@link #getSelectedRoutes()}</li> - * <li>ID should be included in {@link #getDeselectableRoutes()}</li> + * <li>It should be included in {@link #getSelectedRoutes()}</li> + * <li>It should be included in {@link #getDeselectableRoutes()}</li> * </ul> * If the route doesn't meet any of above conditions, it will be ignored. * @@ -1160,11 +1176,9 @@ public final class MediaRouter2 { */ public void deselectRoute(@NonNull MediaRoute2Info route) { Objects.requireNonNull(route, "route must not be null"); - synchronized (mControllerLock) { - if (mIsReleased) { - Log.w(TAG, "deselectRoute: called on released controller. Ignoring."); - return; - } + if (isReleased()) { + Log.w(TAG, "deselectRoute: called on released controller. Ignoring."); + return; } List<MediaRoute2Info> selectedRoutes = getSelectedRoutes(); @@ -1193,13 +1207,8 @@ public final class MediaRouter2 { } /** - * Transfers to a given route for the remote session. The given route must satisfy - * all of the following conditions: - * <ul> - * <li>ID should not be included in {@link RoutingSessionInfo#getSelectedRoutes()}</li> - * <li>ID should be included in {@link RoutingSessionInfo#getTransferableRoutes()}</li> - * </ul> - * If the route doesn't meet any of above conditions, it will be ignored. + * Transfers to a given route for the remote session. The given route must be included + * in {@link RoutingSessionInfo#getTransferableRoutes()}. * * @see RoutingSessionInfo#getSelectedRoutes() * @see RoutingSessionInfo#getTransferableRoutes() @@ -1208,19 +1217,13 @@ public final class MediaRouter2 { void transferToRoute(@NonNull MediaRoute2Info route) { Objects.requireNonNull(route, "route must not be null"); synchronized (mControllerLock) { - if (mIsReleased) { + if (isReleased()) { Log.w(TAG, "transferToRoute: Called on released controller. Ignoring."); return; } - if (mSessionInfo.getSelectedRoutes().contains(route.getId())) { - Log.w(TAG, "Ignoring transferring to a route that is already added. " - + "route=" + route); - return; - } - if (!mSessionInfo.getTransferableRoutes().contains(route.getId())) { - Log.w(TAG, "Ignoring transferring to a non-transferrable route=" + route); + Log.w(TAG, "Ignoring transferring to a non-transferable route=" + route); return; } } @@ -1255,11 +1258,9 @@ public final class MediaRouter2 { return; } - synchronized (mControllerLock) { - if (mIsReleased) { - Log.w(TAG, "setVolume: Called on released controller. Ignoring."); - return; - } + if (isReleased()) { + Log.w(TAG, "setVolume: Called on released controller. Ignoring."); + return; } MediaRouter2Stub stub; synchronized (sRouterLock) { @@ -1275,33 +1276,58 @@ public final class MediaRouter2 { } /** - * Release this controller and corresponding session. + * Releases this controller and the corresponding session. * Any operations on this controller after calling this method will be ignored. * The devices that are playing media will stop playing it. */ - // TODO(b/157872573): Add tests using {@link MediaRouter2Manager#getActiveSessions()}. public void release() { - releaseInternal(/* shouldReleaseSession= */ true, /* shouldNotifyStop= */ true); + releaseInternal(/* shouldReleaseSession= */ true); } /** - * Returns {@code true} when succeeded to release, {@code false} if the controller is - * already released. + * Schedules release of the controller. + * @return {@code true} if it's successfully scheduled, {@code false} if it's already + * scheduled to be released or released. */ - boolean releaseInternal(boolean shouldReleaseSession, boolean shouldNotifyStop) { + boolean scheduleRelease() { synchronized (mControllerLock) { - if (mIsReleased) { - Log.w(TAG, "releaseInternal: Called on released controller. Ignoring."); + if (mState != CONTROLLER_STATE_ACTIVE) { return false; } - mIsReleased = true; + mState = CONTROLLER_STATE_RELEASING; } synchronized (sRouterLock) { + // It could happen if the controller is released by the another thread + // in between two locks if (!mNonSystemRoutingControllers.remove(getId(), this)) { - Log.w(TAG, "releaseInternal: Ignoring unknown controller."); - return false; + // In that case, onStop isn't called so we return true to call onTransfer. + // It's also consistent with that the another thread acquires the lock later. + return true; } + } + + mHandler.postDelayed(this::release, TRANSFER_TIMEOUT_MS); + + return true; + } + + void releaseInternal(boolean shouldReleaseSession) { + boolean shouldNotifyStop; + + synchronized (mControllerLock) { + if (mState == CONTROLLER_STATE_RELEASED) { + if (DEBUG) { + Log.d(TAG, "releaseInternal: Called on released controller. Ignoring."); + } + return; + } + shouldNotifyStop = (mState == CONTROLLER_STATE_ACTIVE); + mState = CONTROLLER_STATE_RELEASED; + } + + synchronized (sRouterLock) { + mNonSystemRoutingControllers.remove(getId(), this); if (shouldReleaseSession && mStub != null) { try { @@ -1326,7 +1352,6 @@ public final class MediaRouter2 { mStub = null; } } - return true; } @Override @@ -1389,9 +1414,14 @@ public final class MediaRouter2 { } @Override - boolean releaseInternal(boolean shouldReleaseSession, boolean shouldNotifyStop) { + boolean scheduleRelease() { + // SystemRoutingController can be always transferred + return true; + } + + @Override + void releaseInternal(boolean shouldReleaseSession) { // Do nothing. SystemRoutingController will never be released - return false; } } @@ -1442,8 +1472,7 @@ public final class MediaRouter2 { if (!(obj instanceof TransferCallbackRecord)) { return false; } - return mTransferCallback - == ((TransferCallbackRecord) obj).mTransferCallback; + return mTransferCallback == ((TransferCallbackRecord) obj).mTransferCallback; } @Override @@ -1481,11 +1510,17 @@ public final class MediaRouter2 { static final class ControllerCreationRequest { public final int mRequestId; + public final long mManagerRequestId; public final MediaRoute2Info mRoute; + public final RoutingController mOldController; - ControllerCreationRequest(int requestId, @NonNull MediaRoute2Info route) { + ControllerCreationRequest(int requestId, long managerRequestId, + @NonNull MediaRoute2Info route, @NonNull RoutingController oldController) { mRequestId = requestId; - mRoute = route; + mManagerRequestId = managerRequestId; + mRoute = Objects.requireNonNull(route, "route must not be null"); + mOldController = Objects.requireNonNull(oldController, + "oldController must not be null"); } } @@ -1534,11 +1569,11 @@ public final class MediaRouter2 { } @Override - public void getSessionHintsForCreatingSession(long uniqueRequestId, - @NonNull MediaRoute2Info route) { + public void requestCreateSessionByManager(long managerRequestId, + RoutingSessionInfo oldSession, MediaRoute2Info route) { mHandler.sendMessage(obtainMessage( - MediaRouter2::onGetControllerHintsForCreatingSessionOnHandler, - MediaRouter2.this, uniqueRequestId, route)); + MediaRouter2::onRequestCreateControllerByManagerOnHandler, + MediaRouter2.this, oldSession, route, managerRequestId)); } } } diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java index dad7859db622..4b09a5f19fb0 100644 --- a/media/java/android/media/MediaRouter2Manager.java +++ b/media/java/android/media/MediaRouter2Manager.java @@ -54,6 +54,12 @@ import java.util.stream.Collectors; public final class MediaRouter2Manager { private static final String TAG = "MR2Manager"; private static final Object sLock = new Object(); + /** + * The request ID for requests not asked by this instance. + * Shouldn't be used for a valid request. + * @hide + */ + public static final int REQUEST_ID_NONE = 0; /** @hide */ @VisibleForTesting public static final int TRANSFER_TIMEOUT_MS = 30_000; @@ -480,7 +486,6 @@ public final class MediaRouter2Manager { notifyTransferFailed(matchingRequest.mOldSessionInfo, requestedRoute); return; } - releaseSession(matchingRequest.mOldSessionInfo); notifyTransferred(matchingRequest.mOldSessionInfo, sessionInfo); } @@ -777,7 +782,7 @@ public final class MediaRouter2Manager { if (client != null) { try { mMediaRouterService.requestCreateSessionWithManager( - client, requestId, oldSession.getClientPackageName(), route); + client, requestId, oldSession, route); } catch (RemoteException ex) { Log.e(TAG, "requestCreateSession: Failed to send a request", ex); } diff --git a/media/java/android/media/RoutingSessionInfo.java b/media/java/android/media/RoutingSessionInfo.java index edf1fc58ecf5..a5d25e0771fd 100644 --- a/media/java/android/media/RoutingSessionInfo.java +++ b/media/java/android/media/RoutingSessionInfo.java @@ -220,7 +220,7 @@ public final class RoutingSessionInfo implements Parcelable { } /** - * Gets information about how volume is handled on the session. + * Gets the information about how volume is handled on the session. * * @return {@link MediaRoute2Info#PLAYBACK_VOLUME_FIXED} or * {@link MediaRoute2Info#PLAYBACK_VOLUME_VARIABLE}. diff --git a/media/jni/android_media_tv_Tuner.cpp b/media/jni/android_media_tv_Tuner.cpp index e8f18a59049e..515d610109ab 100644 --- a/media/jni/android_media_tv_Tuner.cpp +++ b/media/jni/android_media_tv_Tuner.cpp @@ -335,7 +335,7 @@ jobject MediaEvent::getLinearBlock() { if (mLinearBlockObj != NULL) { return mLinearBlockObj; } - mIonHandle = new C2HandleIon(mAvHandle->data[0], mDataLength); + mIonHandle = new C2HandleIon(dup(mAvHandle->data[0]), mDataLength); std::shared_ptr<C2LinearBlock> block = _C2BlockFactory::CreateLinearBlock(mIonHandle); JNIEnv *env = AndroidRuntime::getJNIEnv(); diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java index 0979627e5e8d..ddefe266d897 100644 --- a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java +++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java @@ -126,6 +126,7 @@ public class MediaRouter2ManagerTest { StubMediaRoute2ProviderService instance = StubMediaRoute2ProviderService.getInstance(); if (instance != null) { instance.setProxy(null); + instance.setSpy(null); } } @@ -425,6 +426,79 @@ public class MediaRouter2ManagerTest { @Test @LargeTest + public void testTransferTwice() throws Exception { + Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(FEATURES_ALL); + addRouterCallback(new RouteCallback() { }); + + CountDownLatch successLatch1 = new CountDownLatch(1); + CountDownLatch successLatch2 = new CountDownLatch(1); + CountDownLatch failureLatch = new CountDownLatch(1); + CountDownLatch managerOnSessionReleasedLatch = new CountDownLatch(1); + CountDownLatch serviceOnReleaseSessionLatch = new CountDownLatch(1); + List<RoutingSessionInfo> sessions = new ArrayList<>(); + + StubMediaRoute2ProviderService instance = StubMediaRoute2ProviderService.getInstance(); + assertNotNull(instance); + instance.setSpy(new StubMediaRoute2ProviderService.Spy() { + @Override + public void onReleaseSession(long requestId, String sessionId) { + serviceOnReleaseSessionLatch.countDown(); + } + }); + + addManagerCallback(new MediaRouter2Manager.Callback() { + @Override + public void onTransferred(RoutingSessionInfo oldSession, + RoutingSessionInfo newSession) { + sessions.add(newSession); + if (successLatch1.getCount() > 0) { + successLatch1.countDown(); + } else { + successLatch2.countDown(); + } + } + + @Override + public void onTransferFailed(RoutingSessionInfo session, MediaRoute2Info route) { + failureLatch.countDown(); + } + + @Override + public void onSessionReleased(RoutingSessionInfo session) { + managerOnSessionReleasedLatch.countDown(); + } + }); + + MediaRoute2Info route1 = routes.get(ROUTE_ID1); + MediaRoute2Info route2 = routes.get(ROUTE_ID2); + assertNotNull(route1); + assertNotNull(route2); + + mManager.selectRoute(mPackageName, route1); + assertTrue(successLatch1.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + mManager.selectRoute(mPackageName, route2); + assertTrue(successLatch2.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + + // onTransferFailed/onSessionReleased should not be called. + assertFalse(failureLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS)); + assertFalse(managerOnSessionReleasedLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS)); + + assertEquals(2, sessions.size()); + List<String> activeSessionIds = mManager.getActiveSessions().stream() + .map(RoutingSessionInfo::getId) + .collect(Collectors.toList()); + // The old session shouldn't appear on the active session list. + assertFalse(activeSessionIds.contains(sessions.get(0).getId())); + assertTrue(activeSessionIds.contains(sessions.get(1).getId())); + + assertFalse(serviceOnReleaseSessionLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS)); + mManager.releaseSession(sessions.get(0)); + assertTrue(serviceOnReleaseSessionLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertFalse(managerOnSessionReleasedLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS)); + } + + @Test + @LargeTest public void testTransfer_ignored_fails() throws Exception { Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(FEATURES_ALL); addRouterCallback(new RouteCallback() {}); diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/StubMediaRoute2ProviderService.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/StubMediaRoute2ProviderService.java index 4551876d774a..a51e3714b6f7 100644 --- a/media/tests/MediaRouter/src/com/android/mediaroutertest/StubMediaRoute2ProviderService.java +++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/StubMediaRoute2ProviderService.java @@ -79,6 +79,7 @@ public class StubMediaRoute2ProviderService extends MediaRoute2ProviderService { @GuardedBy("sLock") private static StubMediaRoute2ProviderService sInstance; private Proxy mProxy; + private Spy mSpy; private void initializeRoutes() { MediaRoute2Info route1 = new MediaRoute2Info.Builder(ROUTE_ID1, ROUTE_NAME1) @@ -256,6 +257,11 @@ public class StubMediaRoute2ProviderService extends MediaRoute2ProviderService { @Override public void onReleaseSession(long requestId, String sessionId) { + Spy spy = mSpy; + if (spy != null) { + spy.onReleaseSession(requestId, sessionId); + } + RoutingSessionInfo sessionInfo = getSessionInfo(sessionId); if (sessionInfo == null) { return; @@ -375,7 +381,21 @@ public class StubMediaRoute2ProviderService extends MediaRoute2ProviderService { mProxy = proxy; } + public void setSpy(@Nullable Spy spy) { + mSpy = spy; + } + + /** + * It overrides the original service + */ public static class Proxy { public void onSetRouteVolume(String routeId, int volume, long requestId) {} } + + /** + * It gets notified but doesn't prevent the original methods to be called. + */ + public static class Spy { + public void onReleaseSession(long requestId, String sessionId) {} + } } diff --git a/packages/CarSystemUI/res/layout/notification_center_activity.xml b/packages/CarSystemUI/res/layout/notification_center_activity.xml index 0af74c4462a6..0e45e43132de 100644 --- a/packages/CarSystemUI/res/layout/notification_center_activity.xml +++ b/packages/CarSystemUI/res/layout/notification_center_activity.xml @@ -22,6 +22,10 @@ android:layout_height="match_parent" android:background="@color/notification_shade_background_color"> + <com.android.car.ui.FocusParkingView + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> + <View android:id="@+id/glass_pane" android:layout_width="match_parent" @@ -33,16 +37,20 @@ app:layout_constraintTop_toTopOf="parent" /> - <androidx.recyclerview.widget.RecyclerView - android:id="@+id/notifications" + <com.android.car.ui.FocusArea android:layout_width="0dp" android:layout_height="0dp" android:orientation="vertical" - android:paddingBottom="@dimen/notification_shade_list_padding_bottom" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent"/> + app:layout_constraintTop_toTopOf="parent"> + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/notifications" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingBottom="@dimen/notification_shade_list_padding_bottom"/> + </com.android.car.ui.FocusArea> <include layout="@layout/notification_handle_bar"/> diff --git a/packages/EasterEgg/Android.bp b/packages/EasterEgg/Android.bp index 43ed810b5674..b858ab01ffd9 100644 --- a/packages/EasterEgg/Android.bp +++ b/packages/EasterEgg/Android.bp @@ -23,11 +23,23 @@ android_app { name: "EasterEgg", + platform_apis: true, certificate: "platform", - sdk_version: "current", - optimize: { enabled: false, - } + }, + + static_libs: [ + "androidx.core_core", + "androidx.recyclerview_recyclerview", + "androidx.annotation_annotation", + "kotlinx-coroutines-android", + "kotlinx-coroutines-core", + //"kotlinx-coroutines-reactive", + ], + + manifest: "AndroidManifest.xml", + + kotlincflags: ["-Xjvm-default=enable"], } diff --git a/packages/EasterEgg/AndroidManifest.xml b/packages/EasterEgg/AndroidManifest.xml index 7f76a4529963..57c459b6f0fd 100644 --- a/packages/EasterEgg/AndroidManifest.xml +++ b/packages/EasterEgg/AndroidManifest.xml @@ -6,19 +6,24 @@ <uses-permission android:name="android.permission.WRITE_SETTINGS" /> + <!-- used for cat notifications --> + <uses-permission android:name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME" /> + <!-- used to save cat images --> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + <!-- controls --> + <uses-permission android:name="android.permission.BIND_CONTROLS" /> + <application - android:icon="@drawable/q_icon" + android:icon="@drawable/icon" android:label="@string/app_name"> + <activity android:name=".quares.QuaresActivity" android:icon="@drawable/q_icon" android:label="@string/q_egg_name" + android:exported="true" android:theme="@style/QuaresTheme"> <intent-filter> <action android:name="android.intent.action.MAIN" /> - - <category android:name="android.intent.category.DEFAULT" /> - <!-- <category android:name="android.intent.category.LAUNCHER" /> --> - <category android:name="com.android.internal.category.PLATLOGO" /> </intent-filter> </activity> <activity @@ -26,15 +31,86 @@ android:configChanges="orientation|keyboardHidden|screenSize|uiMode" android:icon="@drawable/p_icon" android:label="@string/p_egg_name" + android:exported="true" android:theme="@style/AppTheme"> <intent-filter> <action android:name="android.intent.action.MAIN" /> + </intent-filter> + </activity> - <!-- <category android:name="android.intent.category.DEFAULT" /> --> - <!-- <category android:name="android.intent.category.LAUNCHER" /> --> - <!-- <category android:name="com.android.internal.category.PLATLOGO" /> --> + <!-- Android N easter egg bits --> + <activity android:name=".neko.NekoLand" + android:theme="@android:style/Theme.Material.NoActionBar" + android:exported="true" + android:label="@string/app_name"> + <intent-filter> + <action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" /> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> + + <!-- This is where the magic happens --> + <service + android:name=".neko.NekoService" + android:enabled="true" + android:permission="android.permission.BIND_JOB_SERVICE" + android:exported="true" > + </service> + + <!-- Used to show over lock screen --> + <activity android:name=".neko.NekoLockedActivity" + android:excludeFromRecents="true" + android:exported="true" + android:theme="@android:style/Theme.Material.Light.Dialog.NoActionBar" + android:showOnLockScreen="true" /> + + <!-- Used to enable easter egg --> + <activity android:name=".neko.NekoActivationActivity" + android:excludeFromRecents="true" + android:exported="true" + android:theme="@android:style/Theme.NoDisplay" + > + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="com.android.internal.category.PLATLOGO" /> + </intent-filter> + </activity> + + <!-- The quick settings tile, disabled by default --> + <service + android:name=".neko.NekoTile" + android:permission="android.permission.BIND_QUICK_SETTINGS_TILE" + android:icon="@drawable/stat_icon" + android:enabled="false" + android:label="@string/default_tile_name"> + <intent-filter> + <action android:name="android.service.quicksettings.action.QS_TILE" /> + </intent-filter> + </service> + + <service android:name=".neko.NekoControlsService" + android:permission="android.permission.BIND_CONTROLS" + android:label="@string/r_egg_name" + android:icon="@drawable/ic_fullcat_icon" + android:enabled="false" + android:exported="true"> + <intent-filter> + <action android:name="android.service.controls.ControlsProviderService" /> + </intent-filter> + </service> + + <!-- FileProvider for sending pictures --> + <provider + android:name="androidx.core.content.FileProvider" + android:authorities="com.android.egg.fileprovider" + android:grantUriPermissions="true" + android:exported="false"> + <meta-data + android:name="android.support.FILE_PROVIDER_PATHS" + android:resource="@xml/filepaths" /> + </provider> </application> </manifest> diff --git a/packages/EasterEgg/build.gradle b/packages/EasterEgg/build.gradle new file mode 100644 index 000000000000..20b469898498 --- /dev/null +++ b/packages/EasterEgg/build.gradle @@ -0,0 +1,82 @@ +buildscript { + ext.kotlin_version = '1.3.71' + + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.0.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' + +final String ANDROID_ROOT = "${rootDir}/../../../.." + +android { + compileSdkVersion COMPILE_SDK + buildToolsVersion BUILD_TOOLS_VERSION + + defaultConfig { + applicationId "com.android.egg" + minSdkVersion 28 + targetSdkVersion 30 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + res.srcDirs = ['res'] + java.srcDirs = ['src'] + manifest.srcFile 'AndroidManifest.xml' + } + } + + signingConfigs { + debug.storeFile file("${ANDROID_ROOT}/vendor/google/certs/devkeys/platform.keystore") + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.core:core-ktx:1.2.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6' + implementation "androidx.recyclerview:recyclerview:${ANDROID_X_VERSION}" + implementation "androidx.dynamicanimation:dynamicanimation:${ANDROID_X_VERSION}" + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + androidTestImplementation "androidx.annotation:annotation:${ANDROID_X_VERSION}" +} + diff --git a/packages/EasterEgg/gradle.properties b/packages/EasterEgg/gradle.properties new file mode 100644 index 000000000000..e8e6450e2943 --- /dev/null +++ b/packages/EasterEgg/gradle.properties @@ -0,0 +1,23 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +android.enableJetifier=true +kotlin.code.style=official + +ANDROID_X_VERSION=1+ +COMPILE_SDK=android-30 +BUILD_TOOLS_VERSION=28.0.3 diff --git a/packages/EasterEgg/res/drawable/android_11_dial.xml b/packages/EasterEgg/res/drawable/android_11_dial.xml new file mode 100644 index 000000000000..73fd37f1bdd6 --- /dev/null +++ b/packages/EasterEgg/res/drawable/android_11_dial.xml @@ -0,0 +1,63 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt" + android:width="108dp" + android:height="108dp" + android:viewportWidth="108" + android:viewportHeight="108"> + <path + android:pathData="M77.773,51.064h-1.583c-0.217,0 -0.393,-0.176 -0.393,-0.393v-1.46c0,-0.217 0.176,-0.393 0.393,-0.393h3.466c0.217,0 0.393,0.176 0.393,0.393v9.921c0,0.217 -0.176,0.393 -0.393,0.393h-1.49c-0.217,0 -0.393,-0.176 -0.393,-0.393V51.064z" + android:fillColor="#F86734"/> + <path + android:pathData="M83.598,51.064h-1.583c-0.217,0 -0.393,-0.176 -0.393,-0.393v-1.46c0,-0.217 0.176,-0.393 0.393,-0.393h3.466c0.217,0 0.393,0.176 0.393,0.393v9.921c0,0.217 -0.176,0.393 -0.393,0.393h-1.49c-0.217,0 -0.393,-0.176 -0.393,-0.393V51.064z" + android:fillColor="#F86734"/> + <path + android:pathData="M70.044,75.974m-0.644,0a0.644,0.644 0,1 1,1.288 0a0.644,0.644 0,1 1,-1.288 0" + android:fillColor="#d7effe"/> + <path + android:pathData="M56.896,80.985m-0.718,0a0.718,0.718 0,1 1,1.436 0a0.718,0.718 0,1 1,-1.436 0" + android:fillColor="#d7effe"/> + <path + android:pathData="M43.408,78.881m-0.795,0a0.795,0.795 0,1 1,1.59 0a0.795,0.795 0,1 1,-1.59 0" + android:fillColor="#d7effe"/> + <path + android:pathData="M32.419,70.115m-0.874,0a0.874,0.874 0,1 1,1.748 0a0.874,0.874 0,1 1,-1.748 0" + android:fillColor="#d7effe"/> + <path + android:pathData="M27.306,56.992m-0.954,0a0.954,0.954 0,1 1,1.908 0a0.954,0.954 0,1 1,-1.908 0" + android:fillColor="#d7effe"/> + <path + android:pathData="M29.313,43.489m-1.036,0a1.036,1.036 0,1 1,2.072 0a1.036,1.036 0,1 1,-2.072 0" + android:fillColor="#d7effe"/> + <path + android:pathData="M37.988,32.445m-1.118,0a1.118,1.118 0,1 1,2.236 0a1.118,1.118 0,1 1,-2.236 0" + android:fillColor="#d7effe"/> + <path + android:pathData="M51.137,27.064m-1.201,0a1.201,1.201 0,1 1,2.402 0a1.201,1.201 0,1 1,-2.402 0" + android:fillColor="#d7effe"/> + <path + android:pathData="M64.553,28.868m-1.284,0a1.284,1.284 0,1 1,2.568 0a1.284,1.284 0,1 1,-2.568 0" + android:fillColor="#d7effe"/> + <path + android:pathData="M75.522,37.652m-1.368,0a1.368,1.368 0,1 1,2.736 0a1.368,1.368 0,1 1,-2.736 0" + android:fillColor="#d7effe"/> + <path + android:pathData="M87.942,115.052l-47.557,-47.557l26.869,-26.87l47.557,47.558z"> + <aapt:attr name="android:fillColor"> + <gradient + android:startY="56.087" + android:startX="55.8464" + android:endY="100.0297" + android:endX="99.7891" + android:type="linear"> + <item android:offset="0" android:color="#3F000000"/> + <item android:offset="1" android:color="#00000000"/> + </gradient> + </aapt:attr> + </path> + <path + android:pathData="M53.928,54.17m-18.999,0a18.999,18.999 0,1 1,37.998 0a18.999,18.999 0,1 1,-37.998 0" + android:fillColor="#3ddc84"/> + <path + android:pathData="M66.353,54.17m-3.185,0a3.185,3.185 0,1 1,6.37 0a3.185,3.185 0,1 1,-6.37 0" + android:fillColor="#FFFFFF"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/back.xml b/packages/EasterEgg/res/drawable/back.xml new file mode 100644 index 000000000000..b55d65cdf76d --- /dev/null +++ b/packages/EasterEgg/res/drawable/back.xml @@ -0,0 +1,22 @@ +<!-- +Copyright (C) 2016 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path android:name="back" android:fillColor="#FF000000" android:pathData="M37.1,22c-1.1,0 -1.9,0.8 -1.9,1.9v5.6c0,1.1 0.8,1.9 1.9,1.9H39v-1.9v-5.6V22H37.1z"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/belly.xml b/packages/EasterEgg/res/drawable/belly.xml new file mode 100644 index 000000000000..8b0e9afac463 --- /dev/null +++ b/packages/EasterEgg/res/drawable/belly.xml @@ -0,0 +1,22 @@ +<!-- +Copyright (C) 2016 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path android:name="belly" android:fillColor="#FF000000" android:pathData="M20.5,25c-3.6,0 -6.5,2.9 -6.5,6.5V38h13v-6.5C27,27.9 24.1,25 20.5,25z"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/body.xml b/packages/EasterEgg/res/drawable/body.xml new file mode 100644 index 000000000000..86087209eff5 --- /dev/null +++ b/packages/EasterEgg/res/drawable/body.xml @@ -0,0 +1,22 @@ +<!-- +Copyright (C) 2016 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path android:name="body" android:fillColor="#FF000000" android:pathData="M9,20h30v18h-30z"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/bowtie.xml b/packages/EasterEgg/res/drawable/bowtie.xml new file mode 100644 index 000000000000..33fa9216712f --- /dev/null +++ b/packages/EasterEgg/res/drawable/bowtie.xml @@ -0,0 +1,22 @@ +<!-- +Copyright (C) 2016 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path android:name="bowtie" android:fillColor="#FF000000" android:pathData="M29,16.8l-10,5l0,-5l10,5z"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/cap.xml b/packages/EasterEgg/res/drawable/cap.xml new file mode 100644 index 000000000000..d8b4cc58a261 --- /dev/null +++ b/packages/EasterEgg/res/drawable/cap.xml @@ -0,0 +1,22 @@ +<!-- +Copyright (C) 2016 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path android:name="cap" android:fillColor="#FF000000" android:pathData="M27.2,3.8c-1,-0.2 -2.1,-0.3 -3.2,-0.3s-2.1,0.1 -3.2,0.3c0.2,1.3 1.5,2.2 3.2,2.2C25.6,6.1 26.9,5.1 27.2,3.8z"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/collar.xml b/packages/EasterEgg/res/drawable/collar.xml new file mode 100644 index 000000000000..5e4d0fd4f886 --- /dev/null +++ b/packages/EasterEgg/res/drawable/collar.xml @@ -0,0 +1,22 @@ +<!-- +Copyright (C) 2016 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path android:name="collar" android:fillColor="#FF000000" android:pathData="M9,18.4h30v1.7h-30z"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/face_spot.xml b/packages/EasterEgg/res/drawable/face_spot.xml new file mode 100644 index 000000000000..a89fb4fdaadd --- /dev/null +++ b/packages/EasterEgg/res/drawable/face_spot.xml @@ -0,0 +1,22 @@ +<!-- +Copyright (C) 2016 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path android:name="face_spot" android:fillColor="#FF000000" android:pathData="M19.5,15.2a4.5,3.2 0,1 0,9 0a4.5,3.2 0,1 0,-9 0z"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/food_bits.xml b/packages/EasterEgg/res/drawable/food_bits.xml new file mode 100644 index 000000000000..1b2bb6f36947 --- /dev/null +++ b/packages/EasterEgg/res/drawable/food_bits.xml @@ -0,0 +1,33 @@ +<!-- +Copyright (C) 2015 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path + android:fillColor="#FF000000" + android:pathData="M19.1,34l-3.5,1.3c-1,0.4,-2.2,-0.1,-2.6,-1.1l-1.2,-3c-0.4,-1,0.1,-2.2,1.1,-2.6l3.5,-1.3c1,-0.4,2.2,0.1,2.6,1.1l1.2,3 C20.6,32.4,20.1,33.6,19.1,34z"/> + <path + android:fillColor="#FF000000" + android:pathData="M25.2,28.1L22.9,28c-0.8,0,-1.5,-0.7,-1.4,-1.6l0.1,-2c0,-0.8,0.7,-1.5,1.6,-1.4l2.4,0.1c0.8,0,1.5,0.7,1.4,1.6l-0.1,2 C26.8,27.5,26.1,28.1,25.2,28.1z"/> + <path + android:fillColor="#FF000000" + android:pathData="M18.7,23.1L16.5,23c-0.5,0,-0.9,-0.4,-0.8,-0.9l0.1,-2.2c0,-0.5,0.4,-0.9,0.9,-0.8l2.2,0.1c0.5,0,0.9,0.4,0.8,0.9 l-0.1,2.2C19.6,22.8,19.2,23.1,18.7,23.1z"/> + <path + android:fillColor="#FF000000" + android:pathData="M32.2,35.3l-3.6,-1.8c-1,-0.5,-1.4,-1.7,-0.9,-2.7l1.6,-3.1c0.5,-1,1.7,-1.4,2.7,-0.9l3.6,1.8c1,0.5,1.4,1.7,0.9,2.7 l-1.6,3.1C34.4,35.4,33.2,35.7,32.2,35.3z"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/food_chicken.xml b/packages/EasterEgg/res/drawable/food_chicken.xml new file mode 100644 index 000000000000..95b2fb55b796 --- /dev/null +++ b/packages/EasterEgg/res/drawable/food_chicken.xml @@ -0,0 +1,39 @@ +<!-- +Copyright (C) 2015 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path + android:fillColor="#FF000000" + android:pathData="M9,12v14h10V11H9z M11.7,16.3c-0.7,0,-1.3,-0.6,-1.3,-1.3s0.6,-1.3,1.3,-1.3S13,14.3,13,15S12.4,16.3,11.7,16.3z"/> + <path + android:fillColor="#FF000000" + android:pathData="M5.7,20.1l1.6,-3.0l-1.6,-3.0l4.4,3.0z"/> + <path + android:fillColor="#FF000000" + android:pathData="M19.0,6.0l-2.3,2.3l-2.7,-2.6l-2.7,2.6l-2.3,-2.3l0.0,4.0l10.0,0.0z"/> + <path + android:fillColor="#FF000000" + android:pathData="M9,25c0,8.3,6.7,15,15,15s15,-6.7,15,-15H9z M29.9,31.5h-11v-1h12L29.9,31.5z M31.9,29.5h-13v-1h14L31.9,29.5z M33.9,27.5 h-15v-1h16L33.9,27.5z"/> + <path + android:fillColor="#FF000000" + android:pathData="M27.0,38.6h2.0v6.0h-2.0z"/> + <path + android:fillColor="#FF000000" + android:pathData="M17.4,44.6l-2.1999998,0.0l4.4000006,-6.0l2.1999989,0.0z"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/food_cookie.xml b/packages/EasterEgg/res/drawable/food_cookie.xml new file mode 100644 index 000000000000..74dd134355e2 --- /dev/null +++ b/packages/EasterEgg/res/drawable/food_cookie.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <group> + <path + android:fillColor="#55FFFFFF" + android:fillType="evenOdd" + android:pathData="M5.71 18.29A8.99 8.99 0 0 0 22 13c0-3-1.46-5.65-3.71-7.29A8.99 8.99 0 0 0 2 11c0 3 1.46 5.65 3.71 7.29z"/> + <path + android:fillColor="#FFFFFFFF" + android:fillType="evenOdd" + android:pathData="M7.25 19.18A8.5 8.5 0 0 0 19.19 7.24 9 9 0 0 1 7.24 19.19z"/> + <path + android:fillColor="#55FFFFFF" + android:pathData="M10.5 3a0.5 0.5 0 1 1 1 0v2.05a0.5 0.5 0 1 1-1 0V3zm3.1 0.42a0.5 0.5 0 0 1 0.93 0.39l-0.8 1.88A0.5 0.5 0 1 1 12.8 5.3l0.8-1.88zm2.7 1.57a0.5 0.5 0 1 1 0.71 0.7l-1.45 1.46a0.5 0.5 0 0 1-0.7-0.71l1.44-1.45zm1.9 2.5a0.5 0.5 0 0 1 0.38 0.92l-1.9 0.77a0.5 0.5 0 0 1-0.37-0.93l1.9-0.77zM19 10.5a0.5 0.5 0 1 1 0 1h-2.05a0.5 0.5 0 0 1 0-1H19zm-0.42 3.1a0.5 0.5 0 0 1-0.39 0.93l-1.88-0.8a0.5 0.5 0 1 1 0.39-0.92l1.88 0.8zm-1.57 2.7a0.5 0.5 0 1 1-0.7 0.71l-1.46-1.45a0.5 0.5 0 0 1 0.71-0.7l1.45 1.44zm-2.5 1.9a0.5 0.5 0 1 1-0.92 0.38l-0.77-1.9a0.5 0.5 0 0 1 0.93-0.37l0.77 1.9zM11.5 19a0.5 0.5 0 1 1-1 0v-2.05a0.5 0.5 0 0 1 1 0V19zm-3.1-0.42a0.5 0.5 0 0 1-0.93-0.39l0.8-1.88A0.5 0.5 0 0 1 9.2 16.7l-0.8 1.88zm-2.7-1.57a0.5 0.5 0 1 1-0.71-0.7l1.45-1.46a0.5 0.5 0 0 1 0.7 0.71L5.7 17.01zm-1.9-2.48a0.5 0.5 0 0 1-0.38-0.92l1.88-0.8a0.5 0.5 0 0 1 0.4 0.92l-1.9 0.8zM3 11.5a0.5 0.5 0 1 1 0-1h2.05a0.5 0.5 0 1 1 0 1H3zm0.42-3.1A0.5 0.5 0 0 1 3.8 7.46l1.88 0.8A0.5 0.5 0 1 1 5.3 9.2L3.42 8.4zm1.57-2.7a0.5 0.5 0 1 1 0.7-0.71l1.46 1.45a0.5 0.5 0 0 1-0.71 0.7L4.99 5.7zm2.5-1.9A0.5 0.5 0 0 1 8.4 3.41l0.77 1.9a0.5 0.5 0 0 1-0.93 0.37L7.48 3.8z"/> + </group> +</vector>
\ No newline at end of file diff --git a/packages/EasterEgg/res/drawable/food_dish.xml b/packages/EasterEgg/res/drawable/food_dish.xml new file mode 100644 index 000000000000..3fff6a90fad2 --- /dev/null +++ b/packages/EasterEgg/res/drawable/food_dish.xml @@ -0,0 +1,24 @@ +<!-- +Copyright (C) 2015 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path + android:fillColor="#FF000000" + android:pathData="M24,13.8C11.3,13.8,1,18.3,1,24c0,5.7,10.3,10.2,23,10.2S47,29.7,47,24C47,18.3,36.7,13.8,24,13.8z M33.7,26.6 c1.1,-0.6,1.8,-1.3,1.8,-2c0,-2.1,-5.2,-3.8,-11.7,-3.8s-11.7,1.7,-11.7,3.8c0,0.6,0.4,1.2,1.2,1.7c-1.7,-0.8,-2.8,-1.7,-2.8,-2.8 c0,-2.5,6,-4.5,13.4,-4.5s13.4,2,13.4,4.5C37.4,24.7,36,25.8,33.7,26.6z"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/food_donut.xml b/packages/EasterEgg/res/drawable/food_donut.xml new file mode 100644 index 000000000000..eaf831ea560c --- /dev/null +++ b/packages/EasterEgg/res/drawable/food_donut.xml @@ -0,0 +1,24 @@ +<!-- +Copyright (C) 2015 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path + android:fillColor="#FF000000" + android:pathData="M24,4.5c-10.5,0,-19,8.5,-19,19s8.5,19,19,19s19,-8.5,19,-19S34.5,4.5,24,4.5z M35.2,15.5l1.6,-1.1 c0.3,-0.2,0.6,-0.1,0.8,0.1l0.1,0.1c0.2,0.3,0.1,0.6,-0.1,0.8l-1.6,1.1c-0.3,0.2,-0.6,0.1,-0.8,-0.1l-0.1,-0.1 C34.9,16.1,35,15.7,35.2,15.5z M32.7,10.7c0,-0.3,0.3,-0.5,0.6,-0.5l0.1,0c0.3,0,0.5,0.3,0.5,0.6l-0.2,2c0,0.3,-0.3,0.5,-0.6,0.5l-0.1,0 c-0.3,0,-0.5,-0.3,-0.5,-0.6L32.7,10.7z M31.7,15.1l1.5,-0.2c0.2,0,0.5,0.1,0.5,0.4l0,0.1c0,0.2,-0.1,0.5,-0.4,0.5l-1.5,0.2 c-0.2,0,-0.5,-0.1,-0.5,-0.4l0,-0.1C31.3,15.4,31.5,15.2,31.7,15.1z M28.8,10.6l1.6,-1.1c0.3,-0.2,0.6,-0.1,0.8,0.1l0.1,0.1 c0.2,0.3,0.1,0.6,-0.1,0.8l-1.6,1.1c-0.3,0.2,-0.6,0.1,-0.8,-0.1l-0.1,-0.1C28.4,11.1,28.5,10.8,28.8,10.6z M25.8,6 c0,-0.3,0.3,-0.5,0.6,-0.5l0.1,0c0.3,0,0.5,0.3,0.5,0.6l-0.2,2c0,0.3,-0.3,0.5,-0.6,0.5l-0.1,0c-0.3,0,-0.5,-0.3,-0.5,-0.6L25.8,6z M20.7,6.5l1.9,-0.7c0.3,-0.1,0.6,0,0.7,0.3l0,0.1c0.1,0.3,0,0.6,-0.3,0.7l-1.9,0.7c-0.3,0.1,-0.6,0,-0.7,-0.3l0,-0.1 C20.3,6.9,20.4,6.6,20.7,6.5z M19.9,10.9l1.5,-0.2c0.2,0,0.5,0.1,0.5,0.4l0,0.1c0,0.2,-0.1,0.5,-0.4,0.5l-1.5,0.2 c-0.2,0,-0.5,-0.1,-0.5,-0.4l0,-0.1C19.5,11.1,19.7,10.9,19.9,10.9z M16,10.9L16,10.9c0.2,-0.3,0.4,-0.4,0.6,-0.3l1.3,0.7 c0.2,0.1,0.3,0.4,0.2,0.6L18,12c-0.1,0.2,-0.4,0.3,-0.6,0.2l-1.3,-0.7C15.9,11.4,15.8,11.1,16,10.9z M15.8,18.5c0.2,0,0.4,0.1,0.5,0.4 l0,0.1c0,0.2,-0.1,0.4,-0.4,0.5l-1.5,0.2c-0.2,0,-0.4,-0.1,-0.5,-0.4l0,-0.1c0,-0.2,0.1,-0.4,0.4,-0.5L15.8,18.5z M14,21.8l-1.6,1.1 c-0.3,0.2,-0.6,0.1,-0.8,-0.1l-0.1,-0.1c-0.2,-0.3,-0.1,-0.6,0.1,-0.8l1.6,-1.1c0.3,-0.2,0.6,-0.1,0.8,0.1l0.1,0.1 C14.3,21.3,14.3,21.6,14,21.8z M12.4,12L12.4,12c0.3,-0.2,0.5,-0.2,0.7,-0.1l1,1.1c0.2,0.2,0.2,0.4,0,0.6L14,13.7 c-0.2,0.2,-0.4,0.2,-0.6,0l-1,-1.1C12.2,12.4,12.2,12.1,12.4,12z M8.3,24.5c0,0.3,-0.3,0.5,-0.6,0.5l-0.1,0c-0.3,0,-0.5,-0.3,-0.5,-0.6 l0.2,-2c0,-0.3,0.3,-0.5,0.6,-0.5l0.1,0c0.3,0,0.5,0.3,0.5,0.6L8.3,24.5z M8.5,16.2v-0.1c0,-0.3,0.2,-0.6,0.6,-0.6h2 c0.3,0,0.6,0.2,0.6,0.6v0.1c0,0.3,-0.2,0.6,-0.6,0.6H9C8.7,16.7,8.5,16.5,8.5,16.2z M10.3,20.7c-0.3,0.2,-0.6,0.1,-0.8,-0.1l-0.1,-0.1 c-0.2,-0.3,-0.1,-0.6,0.1,-0.8l1.6,-1.1c0.3,-0.2,0.6,-0.1,0.8,0.1l0.1,0.1c0.2,0.3,0.1,0.6,-0.1,0.8L10.3,20.7z M11.3,28.3l0,-0.1 c-0.1,-0.3,0,-0.6,0.3,-0.7l1.9,-0.7c0.3,-0.1,0.6,0,0.7,0.3l0,0.1c0.1,0.3,0,0.6,-0.3,0.7L12,28.6C11.7,28.7,11.4,28.6,11.3,28.3z M14.4,33c0,0.2,-0.2,0.4,-0.4,0.4h-1.5c-0.2,0,-0.4,-0.2,-0.4,-0.4v-0.1c0,-0.2,0.2,-0.4,0.4,-0.4H14c0.2,0,0.4,0.2,0.4,0.4V33z M17.9,35.2 l-1.6,1.1c-0.3,0.2,-0.6,0.1,-0.8,-0.1l-0.1,-0.1c-0.2,-0.3,-0.1,-0.6,0.1,-0.8l1.6,-1.1c0.3,-0.2,0.6,-0.1,0.8,0.1l0.1,0.1 C18.2,34.7,18.2,35.1,17.9,35.2z M20.7,33.8l-0.1,0.1c-0.1,0.3,-0.5,0.4,-0.8,0.2l-1.7,-1c-0.3,-0.1,-0.4,-0.5,-0.2,-0.8l0.1,-0.1 c0.1,-0.3,0.5,-0.4,0.8,-0.2l1.7,1C20.7,33.2,20.8,33.5,20.7,33.8z M17.5,23.5c0,-3.6,2.9,-6.5,6.5,-6.5s6.5,2.9,6.5,6.5 c0,3.6,-2.9,6.5,-6.5,6.5S17.5,27.1,17.5,23.5z M27.4,35.7l-1.9,0.7c-0.3,0.1,-0.6,0,-0.7,-0.3l0,-0.1c-0.1,-0.3,0,-0.6,0.3,-0.7l1.9,-0.7 c0.3,-0.1,0.6,0,0.7,0.3l0,0.1C27.9,35.3,27.7,35.6,27.4,35.7z M29.7,32.7l-1.4,0.5c-0.2,0.1,-0.5,0,-0.5,-0.3l0,-0.1 c-0.1,-0.2,0,-0.5,0.3,-0.5l1.4,-0.5c0.2,-0.1,0.5,0,0.5,0.3l0,0.1C30,32.3,29.9,32.6,29.7,32.7z M32.8,35.5l-0.1,0.1 c-0.1,0.3,-0.5,0.4,-0.8,0.2l-1.7,-1c-0.3,-0.1,-0.4,-0.5,-0.2,-0.8l0.1,-0.1c0.1,-0.3,0.5,-0.4,0.8,-0.2l1.7,1C32.8,34.9,32.9,35.2,32.8,35.5z M33.7,30.9c0,0.2,-0.2,0.4,-0.5,0.4l-0.1,0c-0.2,0,-0.4,-0.2,-0.4,-0.5l0.1,-1.5c0,-0.2,0.2,-0.4,0.5,-0.4l0.1,0c0.2,0,0.4,0.2,0.4,0.5 L33.7,30.9z M34.5,26.5l-1.3,0.9c-0.2,0.1,-0.5,0.1,-0.6,-0.1l-0.1,-0.1c-0.1,-0.2,-0.1,-0.5,0.1,-0.6l1.3,-0.9c0.2,-0.1,0.5,-0.1,0.6,0.1 l0.1,0.1C34.8,26.1,34.7,26.3,34.5,26.5z M35.6,20.6l-1.7,-1c-0.3,-0.1,-0.4,-0.5,-0.2,-0.8l0.1,-0.1c0.1,-0.3,0.5,-0.4,0.8,-0.2l1.7,1 c0.3,0.1,0.4,0.5,0.2,0.8l-0.1,0.1C36.2,20.6,35.8,20.7,35.6,20.6z M38.6,27.1l-1.6,1.1c-0.3,0.2,-0.6,0.1,-0.8,-0.1L36.1,28 c-0.2,-0.3,-0.1,-0.6,0.1,-0.8l1.6,-1.1c0.3,-0.2,0.6,-0.1,0.8,0.1l0.1,0.1C38.9,26.6,38.8,27,38.6,27.1z M39,19.4l-1.5,0.2 c-0.2,0,-0.5,-0.1,-0.5,-0.4l0,-0.1c0,-0.2,0.1,-0.5,0.4,-0.5l1.5,-0.2c0.2,0,0.5,0.1,0.5,0.4l0,0.1C39.4,19.1,39.2,19.3,39,19.4z"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/food_sysuituna.xml b/packages/EasterEgg/res/drawable/food_sysuituna.xml new file mode 100644 index 000000000000..28cf4a2c7683 --- /dev/null +++ b/packages/EasterEgg/res/drawable/food_sysuituna.xml @@ -0,0 +1,24 @@ +<!-- +Copyright (C) 2015 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path + android:fillColor="#FF000000" + android:pathData="M46,18.4l-5.8,4.6c-3.9,-3.2,-8.9,-5.6,-14.6,-6.3l1.2,-6l-7.3,5.9C12.5,17.2,6.4,20,2,24.3l7.2,1.4L2,27 c4.3,4.2,10.4,7.1,17.3,7.6l3.1,2.5L22,34.8c7.1,0,13.5,-2.5,18.2,-6.5l5.8,4.6l-1.4,-7.2L46,18.4z M14.3,24.8l-0.6,0.6l-1.1,-1.1 l-1.1,1.1l-0.6,-0.6l1.1,-1.1l-1.1,-1.1l0.6,-0.6l1.1,1.1l1.1,-1.1l0.6,0.6l-1.1,1.1L14.3,24.8z M18.8,29.1c0.7,-0.8,1.1,-2.2,1.1,-3.8 c0,-1.6,-0.4,-3,-1.1,-3.8c1.1,0.5,1.9,2,1.9,3.8S19.9,28.5,18.8,29.1z M20.7,29.1c0.7,-0.8,1.1,-2.2,1.1,-3.8c0,-1.6,-0.4,-3,-1.1,-3.8 c1.1,0.5,1.9,2,1.9,3.8S21.8,28.5,20.7,29.1z"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/foot1.xml b/packages/EasterEgg/res/drawable/foot1.xml new file mode 100644 index 000000000000..0d9085998a18 --- /dev/null +++ b/packages/EasterEgg/res/drawable/foot1.xml @@ -0,0 +1,22 @@ +<!-- +Copyright (C) 2016 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path android:name="foot1" android:fillColor="#FF000000" android:pathData="M11.5,43m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/foot2.xml b/packages/EasterEgg/res/drawable/foot2.xml new file mode 100644 index 000000000000..364ba0cd861c --- /dev/null +++ b/packages/EasterEgg/res/drawable/foot2.xml @@ -0,0 +1,22 @@ +<!-- +Copyright (C) 2016 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path android:name="foot2" android:fillColor="#FF000000" android:pathData="M18.5,43m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/foot3.xml b/packages/EasterEgg/res/drawable/foot3.xml new file mode 100644 index 000000000000..e3a512a2568d --- /dev/null +++ b/packages/EasterEgg/res/drawable/foot3.xml @@ -0,0 +1,22 @@ +<!-- +Copyright (C) 2016 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path android:name="foot3" android:fillColor="#FF000000" android:pathData="M29.5,43m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/foot4.xml b/packages/EasterEgg/res/drawable/foot4.xml new file mode 100644 index 000000000000..66b78fa26649 --- /dev/null +++ b/packages/EasterEgg/res/drawable/foot4.xml @@ -0,0 +1,22 @@ +<!-- +Copyright (C) 2016 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path android:name="foot4" android:fillColor="#FF000000" android:pathData="M36.5,43m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/head.xml b/packages/EasterEgg/res/drawable/head.xml new file mode 100644 index 000000000000..df600a8613cd --- /dev/null +++ b/packages/EasterEgg/res/drawable/head.xml @@ -0,0 +1,22 @@ +<!-- +Copyright (C) 2016 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path android:name="head" android:fillColor="#FF000000" android:pathData="M9,18.5c0,-8.3 6.8,-15 15,-15s15,6.7 15,15H9z"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/ic_bowl.xml b/packages/EasterEgg/res/drawable/ic_bowl.xml new file mode 100644 index 000000000000..d55565d92988 --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_bowl.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2020 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M3,19L21,19" + android:strokeWidth="2" + android:strokeColor="#FF8000"/> + <path + android:pathData="M7,12L4.5,19H19.5L17,12H7Z" + android:strokeLineJoin="round" + android:strokeWidth="2" + android:strokeColor="#FF8000"/> + <path + android:strokeWidth="1" + android:pathData="M7.5257,18.8419L9.5257,12.8419" + android:strokeColor="#FF8000"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/ic_close.xml b/packages/EasterEgg/res/drawable/ic_close.xml new file mode 100644 index 000000000000..60ea36b11fcc --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_close.xml @@ -0,0 +1,24 @@ +<!-- + Copyright (C) 2016 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24.0dp" + android:height="24.0dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M19.0,6.41L17.59,5.0 12.0,10.59 6.41,5.0 5.0,6.41 10.59,12.0 5.0,17.59 6.41,19.0 12.0,13.41 17.59,19.0 19.0,17.59 13.41,12.0z"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/ic_foodbowl_filled.xml b/packages/EasterEgg/res/drawable/ic_foodbowl_filled.xml new file mode 100644 index 000000000000..54961af68aef --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_foodbowl_filled.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2020 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M3,19L21,19" + android:strokeWidth="2" + android:strokeColor="#FF8000"/> + <path + android:pathData="M9,9m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" + android:fillColor="#FF8000"/> + <path + android:pathData="M12,9m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" + android:fillColor="#FF8000"/> + <path + android:pathData="M15,9m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" + android:fillColor="#FF8000"/> + <path + android:pathData="M13.5,7m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" + android:fillColor="#FF8000"/> + <path + android:pathData="M10.5,7m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" + android:fillColor="#FF8000"/> + <path + android:pathData="M6.0583,11.6637C6.2004,11.2657 6.5774,11 7,11H17C17.4226,11 17.7996,11.2657 17.9418,11.6637L19.8476,17H4.1524L6.0583,11.6637ZM7.5,12L6,16H7L8.5,12H7.5Z" + android:fillColor="#FF8000" + android:fillType="evenOdd"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/ic_fullcat_icon.xml b/packages/EasterEgg/res/drawable/ic_fullcat_icon.xml new file mode 100644 index 000000000000..5dca3d18f2d4 --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_fullcat_icon.xml @@ -0,0 +1,108 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2020 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48" + android:viewportHeight="48"> + <path + android:pathData="M15.38,1.02l5.12,5.32l-6.32,2.72l1.2,-8.04z" + android:fillColor="#808080"/> + <path + android:pathData="M32.63,1.02l-5.13,5.32l6.32,2.72l-1.19,-8.04z" + android:fillColor="#808080"/> + <path + android:pathData="M33.82,9.06l-4.77,-1.82l3.58,-6.22l1.19,8.04z" + android:fillColor="#666"/> + <path + android:pathData="M15.38,1.02l3.57,6.22l-4.77,1.82l1.2,-8.04z" + android:fillColor="#666"/> + <path + android:pathData="M9,18.5a15,15 0,0 1,30 0Z" + android:fillColor="#808080"/> + <path + android:pathData="M19.5,15.25a4.5,3.25 0,1 0,9 0a4.5,3.25 0,1 0,-9 0z" + android:fillColor="#fff"/> + <path + android:fillColor="#FF000000" + android:pathData="M20.5,11c0,1.73 -3,1.73 -3,0S20.5,9.35 20.5,11Z"/> + <path + android:fillColor="#FF000000" + android:pathData="M30.5,11c0,1.73 -3,1.73 -3,0S30.5,9.35 30.5,11Z"/> + <path + android:fillColor="#FF000000" + android:pathData="M25.15,13c0,1.28 -2.3,1.28 -2.3,0S25.15,11.73 25.15,13Z"/> + <path + android:pathData="M29,14.29a2.78,2.78 0,0 1,-2.33 1.41A2.75,2.75 0,0 1,24 13" + android:strokeWidth="1.25" + android:fillColor="#00000000" + android:strokeColor="#000" + android:strokeLineCap="round"/> + <path + android:pathData="M24,13a2.66,2.66 0,0 1,-2.67 2.69A2.53,2.53 0,0 1,19 14.29" + android:strokeWidth="1.25" + android:fillColor="#00000000" + android:strokeColor="#000" + android:strokeLineCap="round"/> + <path + android:pathData="M9,20h30v18h-30z" + android:fillColor="#808080"/> + <path + android:pathData="M11.5,43m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0" + android:fillColor="#fff"/> + <path + android:pathData="M9,37h5v6h-5z" + android:fillColor="#808080"/> + <path + android:pathData="M29.5,43m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0" + android:fillColor="#fff"/> + <path + android:pathData="M27,37h5v6h-5z" + android:fillColor="#808080"/> + <path + android:pathData="M36.5,43m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0" + android:fillColor="#fff"/> + <path + android:pathData="M34,37h5v6h-5z" + android:fillColor="#808080"/> + <path + android:pathData="M18.5,43m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0" + android:fillColor="#fff"/> + <path + android:pathData="M16,37h5v6h-5z" + android:fillColor="#808080"/> + <path + android:pathData="M35,35.5h5.9a3.8,3.8 0,0 0,3.8 -3.8V25.5" + android:strokeWidth="5" + android:fillColor="#00000000" + android:strokeColor="#808080" + android:strokeLineCap="round"/> + <path + android:pathData="M40,38l0,-5l-1,0l0,5l1,0z" + android:fillColor="#666"/> + <path + android:pathData="M20.5,25A6.47,6.47 0,0 0,14 31.5V38H27V31.5A6.47,6.47 0,0 0,20.5 25Z" + android:fillColor="#fff"/> + <path + android:pathData="M16,38h5v1h-5z" + android:fillColor="#666"/> + <path + android:pathData="M9,18.5h30v1.5h-30z" + android:fillColor="#3ddc84"/> + <path + android:pathData="M29,16.75l-10,5l0,-5l10,5l0,-5z" + android:fillColor="#3ddc84"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/ic_share.xml b/packages/EasterEgg/res/drawable/ic_share.xml new file mode 100644 index 000000000000..8cebc7ed46de --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_share.xml @@ -0,0 +1,24 @@ +<!-- + Copyright (C) 2016 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24.0dp" + android:height="24.0dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M18.0,16.08c-0.76,0.0 -1.4,0.3 -1.9,0.77L8.91,12.7c0.05,-0.2 0.09,-0.4 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.5,0.5 1.2,0.81 2.0,0.81 1.66,0.0 3.0,-1.34 3.0,-3.0s-1.34,-3.0 -3.0,-3.0 -3.0,1.34 -3.0,3.0c0.0,0.2 0.0,0.4 0.0,0.7L8.04,9.81C7.5,9.31 6.79,9.0 6.0,9.0c-1.66,0.0 -3.0,1.34 -3.0,3.0s1.34,3.0 3.0,3.0c0.79,0.0 1.5,-0.31 2.04,-0.81l7.12,4.16c0.0,0.21 0.0,0.43 0.0,0.65 0.0,1.61 1.31,2.92 2.92,2.92 1.61,0.0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/ic_toy_ball.xml b/packages/EasterEgg/res/drawable/ic_toy_ball.xml new file mode 100644 index 000000000000..411084b2a272 --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_toy_ball.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2020 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M12,12m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#FF4080"/> + <path + android:pathData="M12,9C12.5523,9 13,8.5523 13,8C13,7.4477 12.5523,7 12,7V9ZM7,12C7,12.5523 7.4477,13 8,13C8.5523,13 9,12.5523 9,12H7ZM12,7C10.6748,7 9.4332,7.6526 8.5429,8.5429C7.6526,9.4332 7,10.6748 7,12H9C9,11.3252 9.3475,10.5668 9.9571,9.9571C10.5668,9.3475 11.3252,9 12,9V7Z" + android:fillColor="#FF4080"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/ic_toy_fish.xml b/packages/EasterEgg/res/drawable/ic_toy_fish.xml new file mode 100644 index 000000000000..bb01e9f32bfb --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_toy_fish.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2020 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M14.8492,8.9498C15.7483,9.8489 16.132,11.201 15.9095,12.7581C15.6871,14.3155 14.8589,16.0111 13.435,17.435C12.0111,18.8589 10.3155,19.6871 8.7581,19.9096C7.201,20.132 5.8488,19.7484 4.9497,18.8493C4.0506,17.9501 3.667,16.598 3.8894,15.0409C4.1119,13.4835 4.9401,11.7879 6.364,10.364C7.7879,8.9401 9.4835,8.1119 11.0409,7.8895C12.598,7.667 13.9501,8.0506 14.8492,8.9498Z" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#FF4080"/> + <path + android:pathData="M7,15m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" + android:fillColor="#FF4080"/> + <path + android:pathData="M14.5,8L17.5,3C17.5,3 18,4.5 19,6C20,7.5 21.5,8.5 21.5,8.5L16.5,10" + android:strokeLineJoin="round" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#FF4080"/> + <path + android:pathData="M8.5,4.5L6.5,10L10,7.5L8.5,4.5Z" + android:strokeLineJoin="round" + android:strokeWidth="2" + android:fillColor="#FF4080" + android:strokeColor="#FF4080"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/ic_toy_laser.xml b/packages/EasterEgg/res/drawable/ic_toy_laser.xml new file mode 100644 index 000000000000..8fe84ffbd38c --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_toy_laser.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2020 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M12.866,3.5C12.6874,3.1906 12.3573,3 12,3C11.6427,3 11.3126,3.1906 11.134,3.5L2.4737,18.5C2.2951,18.8094 2.2951,19.1906 2.4737,19.5C2.6523,19.8094 2.9825,20 3.3398,20H20.6603C21.0175,20 21.3476,19.8094 21.5263,19.5C21.7049,19.1906 21.7049,18.8094 21.5263,18.5L12.866,3.5Z" + android:strokeLineJoin="round" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#FF4080"/> + <path + android:pathData="M8,13.5h11v1h-11z" + android:fillColor="#FF4080"/> + <path + android:pathData="M11.5,10h1v8h-1z" + android:fillColor="#FF4080"/> + <path + android:pathData="M8.86,11.4883l0.6283,-0.6283l5.6547,5.6547l-0.6283,0.6283z" + android:fillColor="#FF4080"/> + <path + android:pathData="M9.4883,17.143l-0.6283,-0.6283l5.6547,-5.6547l0.6283,0.6283z" + android:fillColor="#FF4080"/> + <path + android:pathData="M12,14m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0" + android:fillColor="#FF4080"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/ic_toy_mouse.xml b/packages/EasterEgg/res/drawable/ic_toy_mouse.xml new file mode 100644 index 000000000000..ba3dc3322083 --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_toy_mouse.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2020 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M14.8492,8.9498C15.7483,9.8489 16.132,11.201 15.9095,12.7581C15.6871,14.3155 14.8589,16.0111 13.435,17.435C12.0111,18.8589 10.3155,19.6871 8.7581,19.9096C7.201,20.132 5.8488,19.7484 4.9497,18.8493C4.0506,17.9501 3.667,16.598 3.8894,15.0409C4.1119,13.4835 4.9401,11.7879 6.364,10.364C7.7879,8.9401 9.4835,8.1119 11.0409,7.8895C12.598,7.667 13.9501,8.0506 14.8492,8.9498Z" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#FF4080"/> + <path + android:pathData="M3.5,11.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#FF4080"/> + <path + android:pathData="M7.5,7.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#FF4080"/> + <path + android:pathData="M7,15m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" + android:fillColor="#FF4080"/> + <path + android:pathData="M9,13m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" + android:fillColor="#FF4080"/> + <path + android:pathData="M22,4C22,3.4477 21.5523,3 21,3C20.4477,3 20,3.4477 20,4L22,4ZM15,9C14.873,9.9919 14.8735,9.992 14.874,9.992C14.8742,9.9921 14.8747,9.9921 14.8751,9.9922C14.8759,9.9923 14.8768,9.9924 14.8778,9.9925C14.8798,9.9928 14.8821,9.993 14.8848,9.9934C14.8902,9.994 14.8971,9.9948 14.9054,9.9958C14.922,9.9976 14.9442,10 14.9718,10.0026C15.027,10.0079 15.1036,10.0143 15.1985,10.02C15.3881,10.0312 15.6534,10.0396 15.9697,10.0294C16.5957,10.0092 17.455,9.9156 18.3326,9.6062C19.2147,9.2951 20.1482,8.7534 20.8583,7.8203C21.5743,6.8795 22,5.6234 22,4L20,4C20,5.2607 19.6757,6.0717 19.2667,6.6091C18.8518,7.1543 18.2853,7.5021 17.6674,7.72C17.045,7.9395 16.4043,8.0144 15.9053,8.0304C15.6591,8.0384 15.4556,8.0317 15.3171,8.0235C15.248,8.0194 15.1957,8.0149 15.1629,8.0118C15.1466,8.0102 15.1352,8.009 15.129,8.0083C15.126,8.008 15.1242,8.0077 15.1239,8.0077C15.1237,8.0077 15.1239,8.0077 15.1244,8.0078C15.1247,8.0078 15.125,8.0078 15.1254,8.0079C15.1256,8.0079 15.126,8.008 15.1262,8.008C15.1266,8.008 15.127,8.0081 15,9Z" + android:fillColor="#FF4080"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/ic_water.xml b/packages/EasterEgg/res/drawable/ic_water.xml new file mode 100644 index 000000000000..7d94b2409636 --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_water.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2020 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M17.654,7.563L12,2L6.346,7.563C5.6036,8.2877 5.0136,9.1533 4.6108,10.1094C4.2079,11.0654 4.0002,12.0924 4,13.1299C4,15.2516 4.8429,17.2863 6.3432,18.7866C7.8434,20.2869 9.8783,21.1299 12,21.1299C14.1217,21.1299 16.1566,20.2869 17.6569,18.7866C19.1572,17.2863 20,15.2516 20,13.1299C20,12.0924 19.7925,11.0654 19.3896,10.1094C18.9867,9.1533 18.3966,8.2875 17.654,7.563V7.563ZM12,19C10.4265,19.0152 8.9113,18.4056 7.7865,17.3052C6.6617,16.2048 6.0192,14.7033 6,13.1299C5.9996,12.3577 6.1541,11.5933 6.4543,10.8818C6.7546,10.1704 7.1945,9.5262 7.748,8.9878L12,4.8061L16.252,8.9888C16.8056,9.5269 17.2456,10.171 17.5458,10.8823C17.8461,11.5936 18.0005,12.3578 18,13.1299C17.9807,14.7033 17.3383,16.2048 16.2135,17.3052C15.0887,18.4056 13.5735,19.0152 12,19Z" + android:fillColor="#0080FF"/> + <path + android:pathData="M16,12C15.7348,12 15.4804,12.1054 15.2929,12.293C15.1054,12.4805 15,12.7348 15,13C15,13.7956 14.6839,14.5585 14.1213,15.1211C13.5587,15.6837 12.7956,16 12,16C11.7348,16 11.4804,16.1054 11.2929,16.293C11.1054,16.4805 11,16.7348 11,17C11,17.2652 11.1054,17.5195 11.2929,17.707C11.4804,17.8946 11.7348,18 12,18C13.3256,17.9984 14.5964,17.471 15.5338,16.5337C16.4711,15.5964 16.9984,14.3256 17,13C17,12.7348 16.8946,12.4805 16.7071,12.293C16.5196,12.1054 16.2652,12 16,12Z" + android:fillColor="#0080FF"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/ic_water_filled.xml b/packages/EasterEgg/res/drawable/ic_water_filled.xml new file mode 100644 index 000000000000..eed171d05668 --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_water_filled.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2020 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M17.654,7.563L12,2L6.346,7.563C5.6036,8.2877 5.0136,9.1533 4.6108,10.1094C4.2079,11.0654 4.0002,12.0924 4,13.1299C4.0174,15.2343 4.87,17.2458 6.3703,18.7217C7.8705,20.1975 9.8956,21.017 12,21C14.1044,21.017 16.1295,20.1975 17.6297,18.7217C19.13,17.2458 19.9826,15.2343 20,13.1299C20,12.0924 19.7925,11.0654 19.3896,10.1094C18.9867,9.1533 18.3966,8.2875 17.654,7.563V7.563ZM12,18C11.7348,18 11.4804,17.8946 11.2929,17.707C11.1054,17.5195 11,17.2652 11,17C11,16.7348 11.1054,16.4805 11.2929,16.293C11.4804,16.1054 11.7348,16 12,16C12.7956,16 13.5587,15.6837 14.1213,15.1211C14.6839,14.5585 15,13.7956 15,13C15,12.7348 15.1054,12.4805 15.2929,12.293C15.4804,12.1054 15.7348,12 16,12C16.2652,12 16.5196,12.1054 16.7071,12.293C16.8946,12.4805 17,12.7348 17,13C16.9984,14.3256 16.4711,15.5964 15.5338,16.5337C14.5964,17.471 13.3256,17.9984 12,18Z" + android:fillColor="#0080FF"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/ic_waterbowl_filled.xml b/packages/EasterEgg/res/drawable/ic_waterbowl_filled.xml new file mode 100644 index 000000000000..28b1fa824060 --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_waterbowl_filled.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2020 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M3,19L21,19" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#000000"/> + <path + android:pathData="M6.0583,11.6637C6.2004,11.2657 6.5774,11 7,11H17C17.4226,11 17.7996,11.2657 17.9418,11.6637L19.8476,17H4.1524L6.0583,11.6637ZM7.5,12L6,16H7L8.5,12H7.5Z" + android:fillColor="#000000" + android:fillType="evenOdd"/> + <path + android:pathData="M13.4135,6.3907L12,5L10.5865,6.3907C10.4009,6.5719 10.2534,6.7883 10.1527,7.0273C10.052,7.2663 10.0001,7.5231 10,7.7825C10.0044,8.3086 10.2175,8.8115 10.5926,9.1804C10.9676,9.5494 11.4739,9.7543 12,9.75C12.5261,9.7543 13.0324,9.5494 13.4074,9.1804C13.7825,8.8115 13.9956,8.3086 14,7.7825C14,7.5231 13.9481,7.2664 13.8474,7.0273C13.7467,6.7883 13.5991,6.5719 13.4135,6.3907V6.3907ZM12,9C11.9337,9 11.8701,8.9736 11.8232,8.9268C11.7763,8.8799 11.75,8.8163 11.75,8.75C11.75,8.6837 11.7763,8.6201 11.8232,8.5732C11.8701,8.5264 11.9337,8.5 12,8.5C12.1989,8.5 12.3897,8.4209 12.5303,8.2803C12.671,8.1396 12.75,7.9489 12.75,7.75C12.75,7.6837 12.7763,7.6201 12.8232,7.5732C12.8701,7.5264 12.9337,7.5 13,7.5C13.0663,7.5 13.1299,7.5264 13.1768,7.5732C13.2237,7.6201 13.25,7.6837 13.25,7.75C13.2496,8.0814 13.1178,8.3991 12.8834,8.6334C12.6491,8.8678 12.3314,8.9996 12,9Z" + android:fillColor="#000000"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/icon.xml b/packages/EasterEgg/res/drawable/icon.xml new file mode 100644 index 000000000000..7f8d4fa8833f --- /dev/null +++ b/packages/EasterEgg/res/drawable/icon.xml @@ -0,0 +1,19 @@ +<!-- +Copyright (C) 2018 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. +--> +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> + <background android:drawable="@drawable/icon_bg"/> + <foreground android:drawable="@drawable/android_11_dial"/> +</adaptive-icon> diff --git a/packages/EasterEgg/res/drawable/icon_bg.xml b/packages/EasterEgg/res/drawable/icon_bg.xml index 659f98be4f43..31b2a7f9a333 100644 --- a/packages/EasterEgg/res/drawable/icon_bg.xml +++ b/packages/EasterEgg/res/drawable/icon_bg.xml @@ -1,8 +1,7 @@ -<?xml version="1.0" encoding="utf-8"?> <!-- - Copyright (C) 2018 The Android Open Source Project +Copyright (C) 2018 The Android Open Source Project - Licensed under the Apache License, Version 2.0 (the "License"); + 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 @@ -15,4 +14,5 @@ limitations under the License. --> <color xmlns:android="http://schemas.android.com/apk/res/android" - android:color="@color/q_clue_text" /> + android:color="#073042" /> + diff --git a/packages/EasterEgg/res/drawable/left_ear.xml b/packages/EasterEgg/res/drawable/left_ear.xml new file mode 100644 index 000000000000..2b98736df039 --- /dev/null +++ b/packages/EasterEgg/res/drawable/left_ear.xml @@ -0,0 +1,22 @@ +<!-- +Copyright (C) 2016 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path android:name="left_ear" android:fillColor="#FF000000" android:pathData="M15.4,1l5.1000004,5.3l-6.3,2.8000002z"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/left_ear_inside.xml b/packages/EasterEgg/res/drawable/left_ear_inside.xml new file mode 100644 index 000000000000..1d947edc31e2 --- /dev/null +++ b/packages/EasterEgg/res/drawable/left_ear_inside.xml @@ -0,0 +1,22 @@ +<!-- +Copyright (C) 2016 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path android:name="left_ear_inside" android:fillColor="#FF000000" android:pathData="M15.4,1l3.5,6.2l-4.7,1.9z"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/left_eye.xml b/packages/EasterEgg/res/drawable/left_eye.xml new file mode 100644 index 000000000000..4dde1b661393 --- /dev/null +++ b/packages/EasterEgg/res/drawable/left_eye.xml @@ -0,0 +1,22 @@ +<!-- +Copyright (C) 2016 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path android:name="left_eye" android:fillColor="#FF000000" android:pathData="M20.5,11c0,1.7 -3,1.7 -3,0C17.5,9.3 20.5,9.3 20.5,11z"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/leg1.xml b/packages/EasterEgg/res/drawable/leg1.xml new file mode 100644 index 000000000000..d72c746b6232 --- /dev/null +++ b/packages/EasterEgg/res/drawable/leg1.xml @@ -0,0 +1,22 @@ +<!-- +Copyright (C) 2016 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path android:name="leg1" android:fillColor="#FF000000" android:pathData="M9,37h5v6h-5z"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/leg2.xml b/packages/EasterEgg/res/drawable/leg2.xml new file mode 100644 index 000000000000..a772a870af7d --- /dev/null +++ b/packages/EasterEgg/res/drawable/leg2.xml @@ -0,0 +1,22 @@ +<!-- +Copyright (C) 2016 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path android:name="leg2" android:fillColor="#FF000000" android:pathData="M16,37h5v6h-5z"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/leg2_shadow.xml b/packages/EasterEgg/res/drawable/leg2_shadow.xml new file mode 100644 index 000000000000..b01bd6995c0b --- /dev/null +++ b/packages/EasterEgg/res/drawable/leg2_shadow.xml @@ -0,0 +1,22 @@ +<!-- +Copyright (C) 2016 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path android:name="leg2_shadow" android:fillColor="#FF000000" android:pathData="M16,37h5v3h-5z"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/leg3.xml b/packages/EasterEgg/res/drawable/leg3.xml new file mode 100644 index 000000000000..d471236687b5 --- /dev/null +++ b/packages/EasterEgg/res/drawable/leg3.xml @@ -0,0 +1,22 @@ +<!-- +Copyright (C) 2016 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path android:name="leg3" android:fillColor="#FF000000" android:pathData="M27,37h5v6h-5z"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/leg4.xml b/packages/EasterEgg/res/drawable/leg4.xml new file mode 100644 index 000000000000..e5868eb80c59 --- /dev/null +++ b/packages/EasterEgg/res/drawable/leg4.xml @@ -0,0 +1,22 @@ +<!-- +Copyright (C) 2016 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path android:name="leg4" android:fillColor="#FF000000" android:pathData="M34,37h5v6h-5z"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/mouth.xml b/packages/EasterEgg/res/drawable/mouth.xml new file mode 100644 index 000000000000..ddcf2e82f976 --- /dev/null +++ b/packages/EasterEgg/res/drawable/mouth.xml @@ -0,0 +1,27 @@ +<!-- +Copyright (C) 2016 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path android:name="mouth" + android:strokeColor="#FF000000" + android:strokeWidth="1.2" + android:strokeLineCap="round" + android:pathData="M29,14.3c-0.4,0.8 -1.3,1.4 -2.3,1.4c-1.4,0 -2.7,-1.3 -2.7,-2.7 + M24,13c0,1.5 -1.2,2.7 -2.7,2.7c-1,0 -1.9,-0.5 -2.3,-1.4"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/nose.xml b/packages/EasterEgg/res/drawable/nose.xml new file mode 100644 index 000000000000..d403cd1baadf --- /dev/null +++ b/packages/EasterEgg/res/drawable/nose.xml @@ -0,0 +1,22 @@ +<!-- +Copyright (C) 2016 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path android:name="nose" android:fillColor="#FF000000" android:pathData="M25.2,13c0,1.3 -2.3,1.3 -2.3,0S25.2,11.7 25.2,13z"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/octo_bg.xml b/packages/EasterEgg/res/drawable/octo_bg.xml new file mode 100644 index 000000000000..1e46cf434a8b --- /dev/null +++ b/packages/EasterEgg/res/drawable/octo_bg.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <gradient android:angle="-90" + android:startColor="#FF205090" + android:endColor="#FF001040" + android:type="linear" + /> +</shape>
\ No newline at end of file diff --git a/packages/EasterEgg/res/drawable/right_ear.xml b/packages/EasterEgg/res/drawable/right_ear.xml new file mode 100644 index 000000000000..b9fb4d1c7470 --- /dev/null +++ b/packages/EasterEgg/res/drawable/right_ear.xml @@ -0,0 +1,22 @@ +<!-- +Copyright (C) 2016 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path android:name="right_ear" android:fillColor="#FF000000" android:pathData="M32.6,1l-5.0999985,5.3l6.299999,2.8000002z"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/right_ear_inside.xml b/packages/EasterEgg/res/drawable/right_ear_inside.xml new file mode 100644 index 000000000000..86b6e3428d1f --- /dev/null +++ b/packages/EasterEgg/res/drawable/right_ear_inside.xml @@ -0,0 +1,23 @@ +<!-- +Copyright (C) 2016 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + + <path android:name="right_ear_inside" android:fillColor="#FF000000" android:pathData="M33.8,9.1l-4.7,-1.9l3.5,-6.2z"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/right_eye.xml b/packages/EasterEgg/res/drawable/right_eye.xml new file mode 100644 index 000000000000..a1871a62c25b --- /dev/null +++ b/packages/EasterEgg/res/drawable/right_eye.xml @@ -0,0 +1,22 @@ +<!-- +Copyright (C) 2016 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path android:name="right_eye" android:fillColor="#FF000000" android:pathData="M30.5,11c0,1.7 -3,1.7 -3,0C27.5,9.3 30.5,9.3 30.5,11z"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/stat_icon.xml b/packages/EasterEgg/res/drawable/stat_icon.xml new file mode 100644 index 000000000000..608cb2017c3f --- /dev/null +++ b/packages/EasterEgg/res/drawable/stat_icon.xml @@ -0,0 +1,30 @@ +<!-- +Copyright (C) 2015 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FF000000" + android:pathData="M12,2C6.5,2 2,6.5 2,12c0,5.5 4.5,10 10,10s10,-4.5 10,-10C22,6.5 17.5,2 12,2zM5.5,11c0,-1.6 3,-1.6 3,0C8.5,12.7 5.5,12.7 5.5,11zM17.5,14.6c-0.6,1 -1.7,1.7 -2.9,1.7c-1.1,0 -2,-0.6 -2.6,-1.4c-0.6,0.9 -1.6,1.4 -2.7,1.4c-1.3,0 -2.3,-0.7 -2.9,-1.8c-0.2,-0.3 0,-0.7 0.3,-0.8c0.3,-0.2 0.7,0 0.8,0.3c0.3,0.7 1,1.1 1.8,1.1c0.9,0 1.6,-0.5 1.9,-1.3c-0.2,-0.2 -0.4,-0.4 -0.4,-0.7c0,-1.3 2.3,-1.3 2.3,0c0,0.3 -0.2,0.6 -0.4,0.7c0.3,0.8 1.1,1.3 1.9,1.3c0.8,0 1.5,-0.6 1.8,-1.1c0.2,-0.3 0.6,-0.4 0.9,-0.2C17.6,13.9 17.7,14.3 17.5,14.6zM15.5,11c0,-1.6 3,-1.6 3,0C18.5,12.7 15.5,12.7 15.5,11z"/> + <path + android:fillColor="#FF000000" + android:pathData="M5.2,1.0l4.1000004,4.2l-5.0,2.1000004z"/> + <path + android:fillColor="#FF000000" + android:pathData="M18.8,1.0l-4.0999994,4.2l5.000001,2.1000004z"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/tail.xml b/packages/EasterEgg/res/drawable/tail.xml new file mode 100644 index 000000000000..0cca23c3e16c --- /dev/null +++ b/packages/EasterEgg/res/drawable/tail.xml @@ -0,0 +1,26 @@ +<!-- +Copyright (C) 2016 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path android:name="tail" + android:strokeColor="#FF000000" + android:strokeWidth="5" + android:strokeLineCap="round" + android:pathData="M35,35.5h5.9c2.1,0 3.8,-1.7 3.8,-3.8v-6.2"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/tail_cap.xml b/packages/EasterEgg/res/drawable/tail_cap.xml new file mode 100644 index 000000000000..b82f6f9b478a --- /dev/null +++ b/packages/EasterEgg/res/drawable/tail_cap.xml @@ -0,0 +1,22 @@ +<!-- +Copyright (C) 2016 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path android:name="tail_cap" android:fillColor="#FF000000" android:pathData="M42.2,25.5c0,-1.4 1.1,-2.5 2.5,-2.5s2.5,1.1 2.5,2.5H42.2z"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/tail_shadow.xml b/packages/EasterEgg/res/drawable/tail_shadow.xml new file mode 100644 index 000000000000..bb1ff12b3afe --- /dev/null +++ b/packages/EasterEgg/res/drawable/tail_shadow.xml @@ -0,0 +1,22 @@ +<!-- +Copyright (C) 2016 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path android:name="tail_shadow" android:fillColor="#FF000000" android:pathData="M40,38l0,-5l-1,0l0,5z"/> +</vector> diff --git a/packages/EasterEgg/res/layout/activity_paint.xml b/packages/EasterEgg/res/layout/activity_paint.xml index a4c17afd1531..8e916b021bbd 100644 --- a/packages/EasterEgg/res/layout/activity_paint.xml +++ b/packages/EasterEgg/res/layout/activity_paint.xml @@ -16,7 +16,7 @@ --> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" - xmlns:app="http://schemas.android.com/apk/res/com.android.egg" + xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#666" @@ -45,4 +45,4 @@ /> -</FrameLayout>
\ No newline at end of file +</FrameLayout> diff --git a/packages/EasterEgg/res/layout/cat_view.xml b/packages/EasterEgg/res/layout/cat_view.xml new file mode 100644 index 000000000000..85b494d2e68d --- /dev/null +++ b/packages/EasterEgg/res/layout/cat_view.xml @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2016 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. + --> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="?android:attr/listPreferredItemHeightSmall" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:paddingTop="8dp" + android:paddingBottom="8dp" + android:background="?android:attr/selectableItemBackgroundBorderless" + android:gravity="center_horizontal" + android:clipToPadding="false"> + + <FrameLayout + android:layout_width="96dp" + android:layout_height="wrap_content"> + + <ImageView + android:id="@android:id/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="10dp" + android:layout_gravity="center" + android:scaleType="fitCenter" /> + + <LinearLayout + android:id="@+id/contextGroup" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:visibility="invisible" + android:layout_gravity="bottom"> + + <ImageView + android:id="@android:id/shareText" + android:layout_width="40dp" + android:layout_height="40dp" + android:padding="8dp" + android:src="@drawable/ic_share" + android:scaleType="fitCenter" + android:background="#40000000"/> + + <Space + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_weight="1" /> + + <ImageView + android:id="@android:id/closeButton" + android:layout_width="40dp" + android:layout_height="40dp" + android:padding="4dp" + android:src="@drawable/ic_close" + android:scaleType="fitCenter" + android:background="#40000000"/> + + </LinearLayout> + + </FrameLayout> + + <TextView + android:id="@android:id/title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceListItem" + android:gravity="center"/> +</LinearLayout> + diff --git a/packages/EasterEgg/res/layout/edit_text.xml b/packages/EasterEgg/res/layout/edit_text.xml new file mode 100644 index 000000000000..9f7ac802bad4 --- /dev/null +++ b/packages/EasterEgg/res/layout/edit_text.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2016 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. + --> + +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingStart="20dp" + android:paddingEnd="20dp"> + + <EditText + android:id="@android:id/edit" + android:maxLines="1" + android:layout_width="match_parent" + android:layout_height="wrap_content"/> + +</FrameLayout>
\ No newline at end of file diff --git a/packages/EasterEgg/res/layout/food_layout.xml b/packages/EasterEgg/res/layout/food_layout.xml new file mode 100644 index 000000000000..d0ca0c8899aa --- /dev/null +++ b/packages/EasterEgg/res/layout/food_layout.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2016 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. + --> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="?android:attr/selectableItemBackgroundBorderless" + android:paddingLeft="4dp" android:paddingRight="4dp" + android:paddingBottom="6dp" android:paddingTop="6dp"> + <ImageView + android:layout_width="64dp" + android:layout_height="64dp" + android:id="@+id/icon" + android:tint="?android:attr/colorControlNormal"/> + <TextView android:layout_width="64dp" android:layout_height="wrap_content" + android:gravity="top|center_horizontal" + android:id="@+id/text" /> +</LinearLayout> diff --git a/packages/EasterEgg/res/layout/neko_activity.xml b/packages/EasterEgg/res/layout/neko_activity.xml new file mode 100644 index 000000000000..c258137ca710 --- /dev/null +++ b/packages/EasterEgg/res/layout/neko_activity.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright (C) 2016 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. +--> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/holder" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal"/> +</FrameLayout>
\ No newline at end of file diff --git a/packages/EasterEgg/res/values/cat_strings.xml b/packages/EasterEgg/res/values/cat_strings.xml new file mode 100644 index 000000000000..5214fc1ab01d --- /dev/null +++ b/packages/EasterEgg/res/values/cat_strings.xml @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="notification_name" translatable="false">Android Neko</string> + <string name="notification_channel_name" translatable="false">New cats</string> + <string name="default_tile_name" translatable="false">\????</string> + <string name="notification_title" translatable="false">A cat is here.</string> + <string name="default_cat_name" translatable="false">Cat #%s</string> + <string name="directory_name" translatable="false">Cats</string> + <string name="confirm_delete" translatable="false">Forget %s?</string> + <string-array name="food_names" translatable="false"> + <item>Empty dish</item> + <item>Bits</item> + <item>Fish</item> + <item>Chicken</item> + <item>Treat</item> + </string-array> + <array name="food_icons"> + <item>@drawable/food_dish</item> + <item>@drawable/food_bits</item> + <item>@drawable/food_sysuituna</item> + <item>@drawable/food_chicken</item> + <item>@drawable/food_cookie</item> + </array> + <integer-array name="food_intervals"> + <item>0</item> + <item>15</item> + <item>30</item> + <item>60</item> + <item>120</item> + </integer-array> + <integer-array name="food_new_cat_prob"> + <item>0</item> + <item>5</item> + <item>35</item> + <item>65</item> + <item>90</item> + </integer-array> + <string-array name="cat_messages" translatable="false"> + <item>😸</item> + <item>😹</item> + <item>😺</item> + <item>😻</item> + <item>😼</item> + <item>😽</item> + <item>😾</item> + <item>😿</item> + <item>🙀</item> + <item>💩</item> + <item>🐁</item> + </string-array> + <string-array name="rare_cat_messages" translatable="false"> + <item>🍩</item> + <item>🍭</item> + <item>🍫</item> + <item>🍨</item> + <item>🔔</item> + <item>🐝</item> + <item>🍪</item> + <item>🥧</item> + </string-array> + <string name="control_toy_title" translatable="false">Toy</string> + <string name="control_toy_subtitle" translatable="false">Tap to use</string> + <string name="control_toy_status" translatable="false">Cat attracted!</string> + <string name="control_water_title" translatable="false">Water bubbler</string> + <string name="control_water_subtitle" translatable="false">Swipe to fill</string> + <string name="control_food_title" translatable="false">Food bowl</string> + <string name="control_food_subtitle" translatable="false">Tap to refill</string> + <string name="control_food_status_full" translatable="false">Full</string> + <string name="control_food_status_empty" translatable="false">Empty</string> +</resources> + diff --git a/packages/EasterEgg/res/values/dimens.xml b/packages/EasterEgg/res/values/dimens.xml new file mode 100644 index 000000000000..e9dcebd27f7b --- /dev/null +++ b/packages/EasterEgg/res/values/dimens.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright (C) 2016 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. +--> +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + <dimen name="neko_display_size">64dp</dimen> +</resources> diff --git a/packages/EasterEgg/res/values/strings.xml b/packages/EasterEgg/res/values/strings.xml index b95ec6be4c84..25f94215d433 100644 --- a/packages/EasterEgg/res/values/strings.xml +++ b/packages/EasterEgg/res/values/strings.xml @@ -14,11 +14,13 @@ Copyright (C) 2018 The Android Open Source Project limitations under the License. --> <resources xmlns:android="http://schemas.android.com/apk/res/android"> - <string name="app_name" translatable="false">Android Q Easter Egg</string> + <string name="app_name" translatable="false">Android R Easter Egg</string> <!-- name of the Q easter egg, a nonogram-style icon puzzle --> <string name="q_egg_name" translatable="false">Icon Quiz</string> <!-- name of the P easter egg, a humble paint program --> <string name="p_egg_name" translatable="false">PAINT.APK</string> + + <string name="r_egg_name" translatable="false">Cat Controls</string> </resources> diff --git a/packages/EasterEgg/res/xml/filepaths.xml b/packages/EasterEgg/res/xml/filepaths.xml new file mode 100644 index 000000000000..2130025e9265 --- /dev/null +++ b/packages/EasterEgg/res/xml/filepaths.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +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. +--> +<paths> + <external-path name="cats" path="Pictures/Cats" /> +</paths>
\ No newline at end of file diff --git a/packages/EasterEgg/src/com/android/egg/neko/Cat.java b/packages/EasterEgg/src/com/android/egg/neko/Cat.java new file mode 100644 index 000000000000..cd59a735068b --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/neko/Cat.java @@ -0,0 +1,524 @@ +/* + * Copyright (C) 2020 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.egg.neko; + +import static com.android.egg.neko.NekoLand.CHAN_ID; + +import android.app.Notification; +import android.app.PendingIntent; +import android.app.Person; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.PixelFormat; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.os.Bundle; + +import com.android.egg.R; +import com.android.internal.logging.MetricsLogger; + +import java.io.ByteArrayOutputStream; +import java.lang.reflect.InvocationTargetException; +import java.util.List; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; + +/** It's a cat. */ +public class Cat extends Drawable { + public static final long[] PURR = {0, 40, 20, 40, 20, 40, 20, 40, 20, 40, 20, 40}; + + public static final boolean ALL_CATS_IN_ONE_CONVERSATION = true; + + public static final String GLOBAL_SHORTCUT_ID = "com.android.egg.neko:allcats"; + public static final String SHORTCUT_ID_PREFIX = "com.android.egg.neko:cat:"; + + private Random mNotSoRandom; + private Bitmap mBitmap; + private long mSeed; + private String mName; + private int mBodyColor; + private int mFootType; + private boolean mBowTie; + private String mFirstMessage; + + private synchronized Random notSoRandom(long seed) { + if (mNotSoRandom == null) { + mNotSoRandom = new Random(); + mNotSoRandom.setSeed(seed); + } + return mNotSoRandom; + } + + public static final float frandrange(Random r, float a, float b) { + return (b - a) * r.nextFloat() + a; + } + + public static final Object choose(Random r, Object... l) { + return l[r.nextInt(l.length)]; + } + + public static final int chooseP(Random r, int[] a) { + return chooseP(r, a, 1000); + } + + public static final int chooseP(Random r, int[] a, int sum) { + int pct = r.nextInt(sum); + final int stop = a.length - 2; + int i = 0; + while (i < stop) { + pct -= a[i]; + if (pct < 0) break; + i += 2; + } + return a[i + 1]; + } + + public static final int getColorIndex(int q, int[] a) { + for (int i = 1; i < a.length; i += 2) { + if (a[i] == q) { + return i / 2; + } + } + return -1; + } + + public static final int[] P_BODY_COLORS = { + 180, 0xFF212121, // black + 180, 0xFFFFFFFF, // white + 140, 0xFF616161, // gray + 140, 0xFF795548, // brown + 100, 0xFF90A4AE, // steel + 100, 0xFFFFF9C4, // buff + 100, 0xFFFF8F00, // orange + 5, 0xFF29B6F6, // blue..? + 5, 0xFFFFCDD2, // pink!? + 5, 0xFFCE93D8, // purple?!?!? + 4, 0xFF43A047, // yeah, why not green + 1, 0, // ?!?!?! + }; + + public static final int[] P_COLLAR_COLORS = { + 250, 0xFFFFFFFF, + 250, 0xFF000000, + 250, 0xFFF44336, + 50, 0xFF1976D2, + 50, 0xFFFDD835, + 50, 0xFFFB8C00, + 50, 0xFFF48FB1, + 50, 0xFF4CAF50, + }; + + public static final int[] P_BELLY_COLORS = { + 750, 0, + 250, 0xFFFFFFFF, + }; + + public static final int[] P_DARK_SPOT_COLORS = { + 700, 0, + 250, 0xFF212121, + 50, 0xFF6D4C41, + }; + + public static final int[] P_LIGHT_SPOT_COLORS = { + 700, 0, + 300, 0xFFFFFFFF, + }; + + private CatParts D; + + public static void tint(int color, Drawable... ds) { + for (Drawable d : ds) { + if (d != null) { + d.mutate().setTint(color); + } + } + } + + public static boolean isDark(int color) { + final int r = (color & 0xFF0000) >> 16; + final int g = (color & 0x00FF00) >> 8; + final int b = color & 0x0000FF; + return (r + g + b) < 0x80; + } + + public Cat(Context context, long seed) { + D = new CatParts(context); + mSeed = seed; + + setName(context.getString(R.string.default_cat_name, + String.valueOf(mSeed % 1000))); + + final Random nsr = notSoRandom(seed); + + // body color + mBodyColor = chooseP(nsr, P_BODY_COLORS); + if (mBodyColor == 0) mBodyColor = Color.HSVToColor(new float[]{ + nsr.nextFloat() * 360f, frandrange(nsr, 0.5f, 1f), frandrange(nsr, 0.5f, 1f)}); + + tint(mBodyColor, D.body, D.head, D.leg1, D.leg2, D.leg3, D.leg4, D.tail, + D.leftEar, D.rightEar, D.foot1, D.foot2, D.foot3, D.foot4, D.tailCap); + tint(0x20000000, D.leg2Shadow, D.tailShadow); + if (isDark(mBodyColor)) { + tint(0xFFFFFFFF, D.leftEye, D.rightEye, D.mouth, D.nose); + } + tint(isDark(mBodyColor) ? 0xFFEF9A9A : 0x20D50000, D.leftEarInside, D.rightEarInside); + + tint(chooseP(nsr, P_BELLY_COLORS), D.belly); + tint(chooseP(nsr, P_BELLY_COLORS), D.back); + final int faceColor = chooseP(nsr, P_BELLY_COLORS); + tint(faceColor, D.faceSpot); + if (!isDark(faceColor)) { + tint(0xFF000000, D.mouth, D.nose); + } + + mFootType = 0; + if (nsr.nextFloat() < 0.25f) { + mFootType = 4; + tint(0xFFFFFFFF, D.foot1, D.foot2, D.foot3, D.foot4); + } else { + if (nsr.nextFloat() < 0.25f) { + mFootType = 2; + tint(0xFFFFFFFF, D.foot1, D.foot3); + } else if (nsr.nextFloat() < 0.25f) { + mFootType = 3; // maybe -2 would be better? meh. + tint(0xFFFFFFFF, D.foot2, D.foot4); + } else if (nsr.nextFloat() < 0.1f) { + mFootType = 1; + tint(0xFFFFFFFF, (Drawable) choose(nsr, D.foot1, D.foot2, D.foot3, D.foot4)); + } + } + + tint(nsr.nextFloat() < 0.333f ? 0xFFFFFFFF : mBodyColor, D.tailCap); + + final int capColor = chooseP(nsr, isDark(mBodyColor) ? P_LIGHT_SPOT_COLORS : P_DARK_SPOT_COLORS); + tint(capColor, D.cap); + //tint(chooseP(nsr, isDark(bodyColor) ? P_LIGHT_SPOT_COLORS : P_DARK_SPOT_COLORS), D.nose); + + final int collarColor = chooseP(nsr, P_COLLAR_COLORS); + tint(collarColor, D.collar); + mBowTie = nsr.nextFloat() < 0.1f; + tint(mBowTie ? collarColor : 0, D.bowtie); + + String[] messages = context.getResources().getStringArray( + nsr.nextFloat() < 0.1f ? R.array.rare_cat_messages : R.array.cat_messages); + mFirstMessage = (String) choose(nsr, (Object[]) messages); + if (nsr.nextFloat() < 0.5f) mFirstMessage = mFirstMessage + mFirstMessage + mFirstMessage; + } + + public static Cat fromShortcutId(Context context, String shortcutId) { + if (shortcutId.startsWith(SHORTCUT_ID_PREFIX)) { + return new Cat(context, Long.parseLong(shortcutId.replace(SHORTCUT_ID_PREFIX, ""))); + } + return null; + } + + public static Cat create(Context context) { + return new Cat(context, Math.abs(ThreadLocalRandom.current().nextInt())); + } + + public Notification.Builder buildNotification(Context context) { + final Bundle extras = new Bundle(); + extras.putString("android.substName", context.getString(R.string.notification_name)); + + final Icon notificationIcon = createNotificationLargeIcon(context); + + final Intent intent = new Intent(Intent.ACTION_MAIN) + .setClass(context, NekoLand.class) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + ShortcutInfo shortcut = new ShortcutInfo.Builder(context, getShortcutId()) + .setActivity(intent.getComponent()) + .setIntent(intent) + .setShortLabel(getName()) + .setIcon(createShortcutIcon(context)) + .setLongLived(true) + .build(); + context.getSystemService(ShortcutManager.class).addDynamicShortcuts(List.of(shortcut)); + + Notification.BubbleMetadata bubbs = new Notification.BubbleMetadata.Builder() + .setIntent( + PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)) + .setIcon(notificationIcon) + .setSuppressNotification(false) + .setDesiredHeight(context.getResources().getDisplayMetrics().heightPixels) + .build(); + + return new Notification.Builder(context, CHAN_ID) + .setSmallIcon(Icon.createWithResource(context, R.drawable.stat_icon)) + .setLargeIcon(notificationIcon) + .setColor(getBodyColor()) + .setContentTitle(context.getString(R.string.notification_title)) + .setShowWhen(true) + .setCategory(Notification.CATEGORY_STATUS) + .setContentText(getName()) + .setContentIntent( + PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)) + .setAutoCancel(true) + .setStyle(new Notification.MessagingStyle(createPerson()) + .addMessage(mFirstMessage, System.currentTimeMillis(), createPerson()) + .setConversationTitle(getName()) + ) + .setBubbleMetadata(bubbs) + .setShortcutId(getShortcutId()) + .addExtras(extras); + } + + private Person createPerson() { + return new Person.Builder() + .setName(getName()) + .setBot(true) + .setKey(getShortcutId()) + .build(); + } + + public long getSeed() { + return mSeed; + } + + @Override + public void draw(Canvas canvas) { + final int w = Math.min(canvas.getWidth(), canvas.getHeight()); + final int h = w; + + if (mBitmap == null || mBitmap.getWidth() != w || mBitmap.getHeight() != h) { + mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); + final Canvas bitCanvas = new Canvas(mBitmap); + slowDraw(bitCanvas, 0, 0, w, h); + } + canvas.drawBitmap(mBitmap, 0, 0, null); + } + + private void slowDraw(Canvas canvas, int x, int y, int w, int h) { + for (int i = 0; i < D.drawingOrder.length; i++) { + final Drawable d = D.drawingOrder[i]; + if (d != null) { + d.setBounds(x, y, x + w, y + h); + d.draw(canvas); + } + } + + } + + public Bitmap createBitmap(int w, int h) { + if (mBitmap != null && mBitmap.getWidth() == w && mBitmap.getHeight() == h) { + return mBitmap.copy(mBitmap.getConfig(), true); + } + Bitmap result = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); + slowDraw(new Canvas(result), 0, 0, w, h); + return result; + } + + public static Icon recompressIcon(Icon bitmapIcon) { + if (bitmapIcon.getType() != Icon.TYPE_BITMAP) return bitmapIcon; + try { + final Bitmap bits = (Bitmap) Icon.class.getDeclaredMethod("getBitmap").invoke(bitmapIcon); + final ByteArrayOutputStream ostream = new ByteArrayOutputStream( + bits.getWidth() * bits.getHeight() * 2); // guess 50% compression + final boolean ok = bits.compress(Bitmap.CompressFormat.PNG, 100, ostream); + if (!ok) return null; + return Icon.createWithData(ostream.toByteArray(), 0, ostream.size()); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) { + return bitmapIcon; + } + } + + public Icon createNotificationLargeIcon(Context context) { + final Resources res = context.getResources(); + final int w = res.getDimensionPixelSize(android.R.dimen.notification_large_icon_width); + final int h = res.getDimensionPixelSize(android.R.dimen.notification_large_icon_height); + return recompressIcon(createIcon(context, w, h)); + } + + public Icon createShortcutIcon(Context context) { + // shortcuts do not support compressed bitmaps + final Resources res = context.getResources(); + final int w = res.getDimensionPixelSize(android.R.dimen.notification_large_icon_width); + final int h = res.getDimensionPixelSize(android.R.dimen.notification_large_icon_height); + return createIcon(context, w, h); + } + + public Icon createIcon(Context context, int w, int h) { + Bitmap result = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(result); + float[] hsv = new float[3]; + Color.colorToHSV(mBodyColor, hsv); + hsv[2] = (hsv[2] > 0.5f) + ? (hsv[2] - 0.25f) + : (hsv[2] + 0.25f); + //final Paint pt = new Paint(); + //pt.setColor(Color.HSVToColor(hsv)); + //float r = w/2; + //canvas.drawCircle(r, r, r, pt); + // int m = w/10; + + // Adaptive bitmaps! + canvas.drawColor(Color.HSVToColor(hsv)); + int m = w / 4; + + slowDraw(canvas, m, m, w - m - m, h - m - m); + + return Icon.createWithAdaptiveBitmap(result); + } + + @Override + public void setAlpha(int i) { + + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + public String getName() { + return mName; + } + + public void setName(String name) { + this.mName = name; + } + + public int getBodyColor() { + return mBodyColor; + } + + public void logAdd(Context context) { + logCatAction(context, "egg_neko_add"); + } + + public void logRename(Context context) { + logCatAction(context, "egg_neko_rename"); + } + + public void logRemove(Context context) { + logCatAction(context, "egg_neko_remove"); + } + + public void logShare(Context context) { + logCatAction(context, "egg_neko_share"); + } + + private void logCatAction(Context context, String prefix) { + MetricsLogger.count(context, prefix, 1); + MetricsLogger.histogram(context, prefix + "_color", + getColorIndex(mBodyColor, P_BODY_COLORS)); + MetricsLogger.histogram(context, prefix + "_bowtie", mBowTie ? 1 : 0); + MetricsLogger.histogram(context, prefix + "_feet", mFootType); + } + + public String getShortcutId() { + return ALL_CATS_IN_ONE_CONVERSATION + ? GLOBAL_SHORTCUT_ID + : (SHORTCUT_ID_PREFIX + mSeed); + } + + public static class CatParts { + public Drawable leftEar; + public Drawable rightEar; + public Drawable rightEarInside; + public Drawable leftEarInside; + public Drawable head; + public Drawable faceSpot; + public Drawable cap; + public Drawable mouth; + public Drawable body; + public Drawable foot1; + public Drawable leg1; + public Drawable foot2; + public Drawable leg2; + public Drawable foot3; + public Drawable leg3; + public Drawable foot4; + public Drawable leg4; + public Drawable tail; + public Drawable leg2Shadow; + public Drawable tailShadow; + public Drawable tailCap; + public Drawable belly; + public Drawable back; + public Drawable rightEye; + public Drawable leftEye; + public Drawable nose; + public Drawable bowtie; + public Drawable collar; + public Drawable[] drawingOrder; + + public CatParts(Context context) { + body = context.getDrawable(R.drawable.body); + head = context.getDrawable(R.drawable.head); + leg1 = context.getDrawable(R.drawable.leg1); + leg2 = context.getDrawable(R.drawable.leg2); + leg3 = context.getDrawable(R.drawable.leg3); + leg4 = context.getDrawable(R.drawable.leg4); + tail = context.getDrawable(R.drawable.tail); + leftEar = context.getDrawable(R.drawable.left_ear); + rightEar = context.getDrawable(R.drawable.right_ear); + rightEarInside = context.getDrawable(R.drawable.right_ear_inside); + leftEarInside = context.getDrawable(R.drawable.left_ear_inside); + faceSpot = context.getDrawable(R.drawable.face_spot); + cap = context.getDrawable(R.drawable.cap); + mouth = context.getDrawable(R.drawable.mouth); + foot4 = context.getDrawable(R.drawable.foot4); + foot3 = context.getDrawable(R.drawable.foot3); + foot1 = context.getDrawable(R.drawable.foot1); + foot2 = context.getDrawable(R.drawable.foot2); + leg2Shadow = context.getDrawable(R.drawable.leg2_shadow); + tailShadow = context.getDrawable(R.drawable.tail_shadow); + tailCap = context.getDrawable(R.drawable.tail_cap); + belly = context.getDrawable(R.drawable.belly); + back = context.getDrawable(R.drawable.back); + rightEye = context.getDrawable(R.drawable.right_eye); + leftEye = context.getDrawable(R.drawable.left_eye); + nose = context.getDrawable(R.drawable.nose); + collar = context.getDrawable(R.drawable.collar); + bowtie = context.getDrawable(R.drawable.bowtie); + drawingOrder = getDrawingOrder(); + } + + private Drawable[] getDrawingOrder() { + return new Drawable[]{ + collar, + leftEar, leftEarInside, rightEar, rightEarInside, + head, + faceSpot, + cap, + leftEye, rightEye, + nose, mouth, + tail, tailCap, tailShadow, + foot1, leg1, + foot2, leg2, + foot3, leg3, + foot4, leg4, + leg2Shadow, + body, belly, + bowtie + }; + } + } +} diff --git a/packages/EasterEgg/src/com/android/egg/neko/Food.java b/packages/EasterEgg/src/com/android/egg/neko/Food.java new file mode 100644 index 000000000000..aeffc4adfd3a --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/neko/Food.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2020 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.egg.neko; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Icon; + +import com.android.egg.R; + +public class Food { + private final int mType; + + private static int[] sIcons; + private static String[] sNames; + + public Food(int type) { + mType = type; + } + + public Icon getIcon(Context context) { + if (sIcons == null) { + TypedArray icons = context.getResources().obtainTypedArray(R.array.food_icons); + sIcons = new int[icons.length()]; + for (int i = 0; i < sIcons.length; i++) { + sIcons[i] = icons.getResourceId(i, 0); + } + icons.recycle(); + } + return Icon.createWithResource(context, sIcons[mType]); + } + + public String getName(Context context) { + if (sNames == null) { + sNames = context.getResources().getStringArray(R.array.food_names); + } + return sNames[mType]; + } + + public long getInterval(Context context) { + return context.getResources().getIntArray(R.array.food_intervals)[mType]; + } + + public int getType() { + return mType; + } +} diff --git a/packages/EasterEgg/src/com/android/egg/neko/NekoActivationActivity.java b/packages/EasterEgg/src/com/android/egg/neko/NekoActivationActivity.java new file mode 100644 index 000000000000..df461c6878f0 --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/neko/NekoActivationActivity.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2016 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.egg.neko; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.pm.PackageManager; +import android.provider.Settings; +import android.util.Log; +import android.widget.Toast; + +import com.android.internal.logging.MetricsLogger; + +public class NekoActivationActivity extends Activity { + private static final String R_EGG_UNLOCK_SETTING = "egg_mode_r"; + + private void toastUp(String s) { + Toast toast = Toast.makeText(this, s, Toast.LENGTH_SHORT); + toast.show(); + } + + @Override + public void onStart() { + super.onStart(); + + final PackageManager pm = getPackageManager(); + final ComponentName cn = new ComponentName(this, NekoControlsService.class); + final boolean componentEnabled = pm.getComponentEnabledSetting(cn) + == PackageManager.COMPONENT_ENABLED_STATE_ENABLED; + if (Settings.System.getLong(getContentResolver(), + R_EGG_UNLOCK_SETTING, 0) == 0) { + if (componentEnabled) { + Log.v("Neko", "Disabling controls."); + pm.setComponentEnabledSetting(cn, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP); + MetricsLogger.histogram(this, "egg_neko_enable", 0); + toastUp("\uD83D\uDEAB"); + } else { + Log.v("Neko", "Controls already disabled."); + } + } else { + if (!componentEnabled) { + Log.v("Neko", "Enabling controls."); + pm.setComponentEnabledSetting(cn, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, + PackageManager.DONT_KILL_APP); + MetricsLogger.histogram(this, "egg_neko_enable", 1); + toastUp("\uD83D\uDC31"); + } else { + Log.v("Neko", "Controls already enabled."); + } + } + finish(); + } +} diff --git a/packages/EasterEgg/src/com/android/egg/neko/NekoControlsService.kt b/packages/EasterEgg/src/com/android/egg/neko/NekoControlsService.kt new file mode 100644 index 000000000000..56f599a3a219 --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/neko/NekoControlsService.kt @@ -0,0 +1,323 @@ +/* + * Copyright (C) 2020 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.egg.neko + +import android.app.PendingIntent +import android.content.Intent +import android.content.res.ColorStateList +import android.graphics.drawable.Icon +import android.service.controls.Control +import android.service.controls.ControlsProviderService +import android.service.controls.DeviceTypes +import android.service.controls.actions.ControlAction +import android.service.controls.actions.FloatAction +import android.service.controls.templates.ControlButton +import android.service.controls.templates.RangeTemplate +import android.service.controls.templates.StatelessTemplate +import android.service.controls.templates.ToggleTemplate +import android.text.SpannableStringBuilder +import android.text.style.ForegroundColorSpan +import android.util.Log +import androidx.annotation.RequiresApi +import com.android.internal.logging.MetricsLogger +import java.util.Random +import java.util.concurrent.Flow +import java.util.function.Consumer + +import com.android.egg.R + +const val CONTROL_ID_WATER = "water" +const val CONTROL_ID_FOOD = "food" +const val CONTROL_ID_TOY = "toy" + +const val FOOD_SPAWN_CAT_DELAY_MINS = 5L + +const val COLOR_FOOD_FG = 0xFFFF8000.toInt() +const val COLOR_FOOD_BG = COLOR_FOOD_FG and 0x40FFFFFF.toInt() +const val COLOR_WATER_FG = 0xFF0080FF.toInt() +const val COLOR_WATER_BG = COLOR_WATER_FG and 0x40FFFFFF.toInt() +const val COLOR_TOY_FG = 0xFFFF4080.toInt() +const val COLOR_TOY_BG = COLOR_TOY_FG and 0x40FFFFFF.toInt() + +val P_TOY_ICONS = intArrayOf( + 1, R.drawable.ic_toy_mouse, + 1, R.drawable.ic_toy_fish, + 1, R.drawable.ic_toy_ball, + 1, R.drawable.ic_toy_laser +) + +@RequiresApi(30) +fun Control_toString(control: Control): String { + val hc = String.format("0x%08x", control.hashCode()) + return ("Control($hc id=${control.controlId}, type=${control.deviceType}, " + + "title=${control.title}, template=${control.controlTemplate})") +} + +@RequiresApi(30) +public class NekoControlsService : ControlsProviderService(), PrefState.PrefsListener { + private val TAG = "NekoControls" + + private val controls = HashMap<String, Control>() + private val publishers = ArrayList<UglyPublisher>() + private val rng = Random() + + private var lastToyIcon: Icon? = null + + private lateinit var prefs: PrefState + + override fun onCreate() { + super.onCreate() + + prefs = PrefState(this) + prefs.setListener(this) + + createDefaultControls() + } + + override fun onPrefsChanged() { + createDefaultControls() + } + + private fun createDefaultControls() { + val foodState: Int = prefs.foodState + if (foodState != 0) { + NekoService.registerJobIfNeeded(this, FOOD_SPAWN_CAT_DELAY_MINS) + } + + val water = prefs.waterState + + controls[CONTROL_ID_WATER] = makeWaterBowlControl(water) + controls[CONTROL_ID_FOOD] = makeFoodBowlControl(foodState != 0) + controls[CONTROL_ID_TOY] = makeToyControl(currentToyIcon(), false) + } + + private fun currentToyIcon(): Icon { + val icon = lastToyIcon ?: randomToyIcon() + lastToyIcon = icon + return icon + } + + private fun randomToyIcon(): Icon { + return Icon.createWithResource(resources, Cat.chooseP(rng, P_TOY_ICONS, 4)) + } + + private fun colorize(s: CharSequence, color: Int): CharSequence { + val ssb = SpannableStringBuilder(s) + ssb.setSpan(ForegroundColorSpan(color), 0, s.length, 0) + return ssb + } + + private fun makeToyControl(icon: Icon?, thrown: Boolean): Control { + return Control.StatefulBuilder(CONTROL_ID_TOY, getPendingIntent()) + .setDeviceType(DeviceTypes.TYPE_UNKNOWN) + .setCustomIcon(icon) + // ?.setTint(COLOR_TOY_FG)) // TODO(b/159559045): uncomment when fixed + .setCustomColor(ColorStateList.valueOf(COLOR_TOY_BG)) + .setTitle(colorize(getString(R.string.control_toy_title), COLOR_TOY_FG)) + .setStatusText(colorize( + if (thrown) getString(R.string.control_toy_status) else "", + COLOR_TOY_FG)) + .setControlTemplate(StatelessTemplate("toy")) + .setStatus(Control.STATUS_OK) + .setSubtitle(if (thrown) "" else getString(R.string.control_toy_subtitle)) + .setAppIntent(getAppIntent()) + .build() + } + + private fun makeWaterBowlControl(fillLevel: Float): Control { + return Control.StatefulBuilder(CONTROL_ID_WATER, getPendingIntent()) + .setDeviceType(DeviceTypes.TYPE_KETTLE) + .setTitle(colorize(getString(R.string.control_water_title), COLOR_WATER_FG)) + .setCustomColor(ColorStateList.valueOf(COLOR_WATER_BG)) + .setCustomIcon(Icon.createWithResource(resources, + if (fillLevel >= 100f) R.drawable.ic_water_filled else R.drawable.ic_water)) + //.setTint(COLOR_WATER_FG)) // TODO(b/159559045): uncomment when fixed + .setControlTemplate(RangeTemplate("waterlevel", 0f, 200f, fillLevel, 10f, + "%.0f mL")) + .setStatus(Control.STATUS_OK) + .setSubtitle(if (fillLevel == 0f) getString(R.string.control_water_subtitle) else "") + .build() + } + + private fun makeFoodBowlControl(filled: Boolean): Control { + return Control.StatefulBuilder(CONTROL_ID_FOOD, getPendingIntent()) + .setDeviceType(DeviceTypes.TYPE_UNKNOWN) + .setCustomColor(ColorStateList.valueOf(COLOR_FOOD_BG)) + .setTitle(colorize(getString(R.string.control_food_title), COLOR_FOOD_FG)) + .setCustomIcon(Icon.createWithResource(resources, + if (filled) R.drawable.ic_foodbowl_filled else R.drawable.ic_bowl)) + // .setTint(COLOR_FOOD_FG)) // TODO(b/159559045): uncomment when fixed + .setStatusText( + if (filled) colorize( + getString(R.string.control_food_status_full), 0xCCFFFFFF.toInt()) + else colorize( + getString(R.string.control_food_status_empty), 0x80FFFFFF.toInt())) + .setControlTemplate(ToggleTemplate("foodbowl", ControlButton(filled, "Refill"))) + .setStatus(Control.STATUS_OK) + .setSubtitle(if (filled) "" else getString(R.string.control_food_subtitle)) + .build() + } + + private fun getPendingIntent(): PendingIntent { + val intent = Intent(Intent.ACTION_MAIN) + .setClass(this, NekoLand::class.java) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + return PendingIntent.getActivity(this, 0, intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + } + + private fun getAppIntent(): PendingIntent { + return getPendingIntent() + } + + + override fun performControlAction( + controlId: String, + action: ControlAction, + consumer: Consumer<Int> + ) { + when (controlId) { + CONTROL_ID_FOOD -> { + // refill bowl + controls[CONTROL_ID_FOOD] = makeFoodBowlControl(true) + Log.v(TAG, "Bowl refilled. (Registering job.)") + NekoService.registerJob(this, FOOD_SPAWN_CAT_DELAY_MINS) + MetricsLogger.histogram(this, "egg_neko_offered_food", 11) + prefs.foodState = 11 + } + CONTROL_ID_TOY -> { + Log.v(TAG, "Toy tossed.") + controls[CONTROL_ID_TOY] = + makeToyControl(currentToyIcon(), true) + // TODO: re-enable toy + Thread() { + Thread.sleep((1 + Random().nextInt(4)) * 1000L) + NekoService.getExistingCat(prefs)?.let { + NekoService.notifyCat(this, it) + } + controls[CONTROL_ID_TOY] = makeToyControl(randomToyIcon(), false) + pushControlChanges() + }.start() + } + CONTROL_ID_WATER -> { + if (action is FloatAction) { + controls[CONTROL_ID_WATER] = makeWaterBowlControl(action.newValue) + Log.v(TAG, "Water level set to " + action.newValue) + prefs.waterState = action.newValue + } + } + else -> { + return + } + } + consumer.accept(ControlAction.RESPONSE_OK) + pushControlChanges() + } + + private fun pushControlChanges() { + Thread() { + publishers.forEach { it.refresh() } + }.start() + } + + private fun makeStateless(c: Control?): Control? { + if (c == null) return null + return Control.StatelessBuilder(c.controlId, c.appIntent) + .setTitle(c.title) + .setSubtitle(c.subtitle) + .setStructure(c.structure) + .setDeviceType(c.deviceType) + .setCustomIcon(c.customIcon) + .setCustomColor(c.customColor) + .build() + } + + override fun createPublisherFor(list: MutableList<String>): Flow.Publisher<Control> { + createDefaultControls() + + val publisher = UglyPublisher(list, true) + publishers.add(publisher) + return publisher + } + + override fun createPublisherForAllAvailable(): Flow.Publisher<Control> { + createDefaultControls() + + val publisher = UglyPublisher(controls.keys, false) + publishers.add(publisher) + return publisher + } + + private inner class UglyPublisher( + val controlKeys: Iterable<String>, + val indefinite: Boolean + ) : Flow.Publisher<Control> { + val subscriptions = ArrayList<UglySubscription>() + + private inner class UglySubscription( + val initialControls: Iterator<Control>, + var subscriber: Flow.Subscriber<in Control>? + ) : Flow.Subscription { + override fun cancel() { + Log.v(TAG, "cancel subscription: $this for subscriber: $subscriber " + + "to publisher: $this@UglyPublisher") + subscriber = null + unsubscribe(this) + } + + override fun request(p0: Long) { + (0 until p0).forEach { _ -> + if (initialControls.hasNext()) { + send(initialControls.next()) + } else { + if (!indefinite) subscriber?.onComplete() + } + } + } + + fun send(c: Control) { + Log.v(TAG, "sending update: " + Control_toString(c) + " => " + subscriber) + subscriber?.onNext(c) + } + } + + override fun subscribe(subscriber: Flow.Subscriber<in Control>) { + Log.v(TAG, "subscribe to publisher: $this by subscriber: $subscriber") + val sub = UglySubscription(controlKeys.mapNotNull { controls[it] }.iterator(), + subscriber) + subscriptions.add(sub) + subscriber.onSubscribe(sub) + } + + fun unsubscribe(sub: UglySubscription) { + Log.v(TAG, "no more subscriptions, removing subscriber: $sub") + subscriptions.remove(sub) + if (subscriptions.size == 0) { + Log.v(TAG, "no more subscribers, removing publisher: $this") + publishers.remove(this) + } + } + + fun refresh() { + controlKeys.mapNotNull { controls[it] }.forEach { control -> + subscriptions.forEach { sub -> + sub.send(control) + } + } + } + } +} diff --git a/packages/EasterEgg/src/com/android/egg/neko/NekoDialog.java b/packages/EasterEgg/src/com/android/egg/neko/NekoDialog.java new file mode 100644 index 000000000000..2bd2228e7bf2 --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/neko/NekoDialog.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2020 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.egg.neko; + +import android.app.Dialog; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.android.egg.R; + +import java.util.ArrayList; + +public class NekoDialog extends Dialog { + + private final Adapter mAdapter; + + public NekoDialog(@NonNull Context context) { + super(context, android.R.style.Theme_Material_Dialog_NoActionBar); + RecyclerView view = new RecyclerView(getContext()); + mAdapter = new Adapter(getContext()); + view.setLayoutManager(new GridLayoutManager(getContext(), 2)); + view.setAdapter(mAdapter); + final float dp = context.getResources().getDisplayMetrics().density; + final int pad = (int)(16*dp); + view.setPadding(pad, pad, pad, pad); + setContentView(view); + } + + private void onFoodSelected(Food food) { + PrefState prefs = new PrefState(getContext()); + int currentState = prefs.getFoodState(); + if (currentState == 0 && food.getType() != 0) { + NekoService.registerJob(getContext(), food.getInterval(getContext())); + } +// MetricsLogger.histogram(getContext(), "egg_neko_offered_food", food.getType()); + prefs.setFoodState(food.getType()); + dismiss(); + } + + private class Adapter extends RecyclerView.Adapter<Holder> { + + private final Context mContext; + private final ArrayList<Food> mFoods = new ArrayList<>(); + + public Adapter(Context context) { + mContext = context; + int[] foods = context.getResources().getIntArray(R.array.food_names); + // skip food 0, you can't choose it + for (int i=1; i<foods.length; i++) { + mFoods.add(new Food(i)); + } + } + + @Override + public Holder onCreateViewHolder(ViewGroup parent, int viewType) { + return new Holder(LayoutInflater.from(parent.getContext()) + .inflate(R.layout.food_layout, parent, false)); + } + + @Override + public void onBindViewHolder(final Holder holder, int position) { + final Food food = mFoods.get(position); + ((ImageView) holder.itemView.findViewById(R.id.icon)) + .setImageIcon(food.getIcon(mContext)); + ((TextView) holder.itemView.findViewById(R.id.text)) + .setText(food.getName(mContext)); + holder.itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onFoodSelected(mFoods.get(holder.getAdapterPosition())); + } + }); + } + + @Override + public int getItemCount() { + return mFoods.size(); + } + } + + public static class Holder extends RecyclerView.ViewHolder { + + public Holder(View itemView) { + super(itemView); + } + } +} diff --git a/packages/EasterEgg/src/com/android/egg/neko/NekoLand.java b/packages/EasterEgg/src/com/android/egg/neko/NekoLand.java new file mode 100644 index 000000000000..8ed808760dcd --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/neko/NekoLand.java @@ -0,0 +1,340 @@ +/* + * Copyright (C) 2020 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.egg.neko; + +import android.Manifest; +import android.app.ActionBar; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.media.MediaScannerConnection; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.util.Log; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnLongClickListener; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.core.content.FileProvider; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.android.egg.R; +import com.android.egg.neko.PrefState.PrefsListener; +import com.android.internal.logging.MetricsLogger; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +public class NekoLand extends Activity implements PrefsListener { + public static String CHAN_ID = "EGG"; + + public static boolean DEBUG = false; + public static boolean DEBUG_NOTIFICATIONS = false; + + private static final int EXPORT_BITMAP_SIZE = 600; + + private static final int STORAGE_PERM_REQUEST = 123; + + private static boolean CAT_GEN = false; + private PrefState mPrefs; + private CatAdapter mAdapter; + private Cat mPendingShareCat; + + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.neko_activity); + final ActionBar actionBar = getActionBar(); + if (actionBar != null) { + actionBar.setLogo(Cat.create(this)); + actionBar.setDisplayUseLogoEnabled(false); + actionBar.setDisplayShowHomeEnabled(true); + } + + mPrefs = new PrefState(this); + mPrefs.setListener(this); + final RecyclerView recyclerView = findViewById(R.id.holder); + mAdapter = new CatAdapter(); + recyclerView.setAdapter(mAdapter); + recyclerView.setLayoutManager(new GridLayoutManager(this, 3)); + int numCats = updateCats(); + MetricsLogger.histogram(this, "egg_neko_visit_gallery", numCats); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + mPrefs.setListener(null); + } + + private int updateCats() { + Cat[] cats; + if (CAT_GEN) { + cats = new Cat[50]; + for (int i = 0; i < cats.length; i++) { + cats[i] = Cat.create(this); + } + } else { + final float[] hsv = new float[3]; + List<Cat> list = mPrefs.getCats(); + Collections.sort(list, new Comparator<Cat>() { + @Override + public int compare(Cat cat, Cat cat2) { + Color.colorToHSV(cat.getBodyColor(), hsv); + float bodyH1 = hsv[0]; + Color.colorToHSV(cat2.getBodyColor(), hsv); + float bodyH2 = hsv[0]; + return Float.compare(bodyH1, bodyH2); + } + }); + cats = list.toArray(new Cat[0]); + } + mAdapter.setCats(cats); + return cats.length; + } + + private void onCatClick(Cat cat) { + if (CAT_GEN) { + mPrefs.addCat(cat); + new AlertDialog.Builder(NekoLand.this) + .setTitle("Cat added") + .setPositiveButton(android.R.string.ok, null) + .show(); + } else { + showNameDialog(cat); + } + } + + private void onCatRemove(Cat cat) { + cat.logRemove(this); + mPrefs.removeCat(cat); + } + + private void showNameDialog(final Cat cat) { + final Context context = new ContextThemeWrapper(this, + android.R.style.Theme_Material_Light_Dialog_NoActionBar); + // TODO: Move to XML, add correct margins. + View view = LayoutInflater.from(context).inflate(R.layout.edit_text, null); + final EditText text = (EditText) view.findViewById(android.R.id.edit); + text.setText(cat.getName()); + text.setSelection(cat.getName().length()); + final int size = context.getResources() + .getDimensionPixelSize(android.R.dimen.app_icon_size); + Drawable catIcon = cat.createIcon(this, size, size).loadDrawable(this); + new AlertDialog.Builder(context) + .setTitle(" ") + .setIcon(catIcon) + .setView(view) + .setPositiveButton(android.R.string.ok, new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + cat.logRename(context); + cat.setName(text.getText().toString().trim()); + mPrefs.addCat(cat); + } + }).show(); + } + + @Override + public void onPrefsChanged() { + updateCats(); + } + + private class CatAdapter extends RecyclerView.Adapter<CatHolder> { + + private Cat[] mCats; + + public void setCats(Cat[] cats) { + mCats = cats; + notifyDataSetChanged(); + } + + @Override + public CatHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return new CatHolder(LayoutInflater.from(parent.getContext()) + .inflate(R.layout.cat_view, parent, false)); + } + + private void setContextGroupVisible(final CatHolder holder, boolean vis) { + final View group = holder.contextGroup; + if (vis && group.getVisibility() != View.VISIBLE) { + group.setAlpha(0); + group.setVisibility(View.VISIBLE); + group.animate().alpha(1.0f).setDuration(333); + Runnable hideAction = new Runnable() { + @Override + public void run() { + setContextGroupVisible(holder, false); + } + }; + group.setTag(hideAction); + group.postDelayed(hideAction, 5000); + } else if (!vis && group.getVisibility() == View.VISIBLE) { + group.removeCallbacks((Runnable) group.getTag()); + group.animate().alpha(0f).setDuration(250).withEndAction(new Runnable() { + @Override + public void run() { + group.setVisibility(View.INVISIBLE); + } + }); + } + } + + @Override + public void onBindViewHolder(final CatHolder holder, int position) { + Context context = holder.itemView.getContext(); + final int size = context.getResources().getDimensionPixelSize(R.dimen.neko_display_size); + holder.imageView.setImageIcon(mCats[position].createIcon(context, size, size)); + holder.textView.setText(mCats[position].getName()); + holder.itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onCatClick(mCats[holder.getAdapterPosition()]); + } + }); + holder.itemView.setOnLongClickListener(new OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + setContextGroupVisible(holder, true); + return true; + } + }); + holder.delete.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + setContextGroupVisible(holder, false); + new AlertDialog.Builder(NekoLand.this) + .setTitle(getString(R.string.confirm_delete, mCats[position].getName())) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + onCatRemove(mCats[holder.getAdapterPosition()]); + } + }) + .show(); + } + }); + holder.share.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + setContextGroupVisible(holder, false); + Cat cat = mCats[holder.getAdapterPosition()]; + if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + mPendingShareCat = cat; + requestPermissions( + new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, + STORAGE_PERM_REQUEST); + return; + } + shareCat(cat); + } + }); + } + + @Override + public int getItemCount() { + return mCats.length; + } + } + + private void shareCat(Cat cat) { + final File dir = new File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), + "Cats"); + if (!dir.exists() && !dir.mkdirs()) { + Log.e("NekoLand", "save: error: can't create Pictures directory"); + return; + } + final File png = new File(dir, cat.getName().replaceAll("[/ #:]+", "_") + ".png"); + Bitmap bitmap = cat.createBitmap(EXPORT_BITMAP_SIZE, EXPORT_BITMAP_SIZE); + if (bitmap != null) { + try { + OutputStream os = new FileOutputStream(png); + bitmap.compress(Bitmap.CompressFormat.PNG, 0, os); + os.close(); + MediaScannerConnection.scanFile( + this, + new String[]{png.toString()}, + new String[]{"image/png"}, + null); + Log.v("Neko", "cat file: " + png); + Uri uri = FileProvider.getUriForFile(this, "com.android.egg.fileprovider", png); + Log.v("Neko", "cat uri: " + uri); + Intent intent = new Intent(Intent.ACTION_SEND); + intent.putExtra(Intent.EXTRA_STREAM, uri); + intent.putExtra(Intent.EXTRA_SUBJECT, cat.getName()); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.setType("image/png"); + startActivity(Intent.createChooser(intent, null) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)); + cat.logShare(this); + } catch (IOException e) { + Log.e("NekoLand", "save: error: " + e); + } + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, + String permissions[], int[] grantResults) { + if (requestCode == STORAGE_PERM_REQUEST) { + if (mPendingShareCat != null) { + shareCat(mPendingShareCat); + mPendingShareCat = null; + } + } + } + + private static class CatHolder extends RecyclerView.ViewHolder { + private final ImageView imageView; + private final TextView textView; + private final View contextGroup; + private final View delete; + private final View share; + + public CatHolder(View itemView) { + super(itemView); + imageView = (ImageView) itemView.findViewById(android.R.id.icon); + textView = (TextView) itemView.findViewById(android.R.id.title); + contextGroup = itemView.findViewById(R.id.contextGroup); + delete = itemView.findViewById(android.R.id.closeButton); + share = itemView.findViewById(android.R.id.shareText); + } + } +} diff --git a/packages/EasterEgg/src/com/android/egg/neko/NekoLockedActivity.java b/packages/EasterEgg/src/com/android/egg/neko/NekoLockedActivity.java new file mode 100644 index 000000000000..ca89adc7b6e4 --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/neko/NekoLockedActivity.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2020 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.egg.neko; + +import android.app.Activity; +import android.content.DialogInterface; +import android.content.DialogInterface.OnDismissListener; +import android.os.Bundle; +import android.view.WindowManager; + +import androidx.annotation.Nullable; + +public class NekoLockedActivity extends Activity implements OnDismissListener { + + private NekoDialog mDialog; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); + + mDialog = new NekoDialog(this); + mDialog.setOnDismissListener(this); + mDialog.show(); + } + + @Override + public void onDismiss(DialogInterface dialog) { + finish(); + } +} diff --git a/packages/EasterEgg/src/com/android/egg/neko/NekoService.java b/packages/EasterEgg/src/com/android/egg/neko/NekoService.java new file mode 100644 index 000000000000..939e85c07d06 --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/neko/NekoService.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2020 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.egg.neko; + +import static com.android.egg.neko.Cat.PURR; +import static com.android.egg.neko.NekoLand.CHAN_ID; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.app.job.JobService; +import android.content.ComponentName; +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; + +import com.android.egg.R; + +import java.util.List; +import java.util.Random; + +public class NekoService extends JobService { + + private static final String TAG = "NekoService"; + + public static int JOB_ID = 42; + + public static int CAT_NOTIFICATION = 1; + public static int DEBUG_NOTIFICATION = 1234; + + public static float CAT_CAPTURE_PROB = 1.0f; // generous + + public static long SECONDS = 1000; + public static long MINUTES = 60 * SECONDS; + + //public static long INTERVAL_FLEX = 15 * SECONDS; + public static long INTERVAL_FLEX = 5 * MINUTES; + + public static float INTERVAL_JITTER_FRAC = 0.25f; + + private static void setupNotificationChannels(Context context) { + NotificationManager noman = context.getSystemService(NotificationManager.class); + NotificationChannel eggChan = new NotificationChannel(CHAN_ID, + context.getString(R.string.notification_channel_name), + NotificationManager.IMPORTANCE_DEFAULT); + eggChan.setSound(Uri.EMPTY, Notification.AUDIO_ATTRIBUTES_DEFAULT); // cats are quiet + eggChan.setVibrationPattern(PURR); // not totally quiet though + //eggChan.setBlockableSystem(true); // unlike a real cat, you can push this one off your lap + eggChan.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC); // cats sit in the window + noman.createNotificationChannel(eggChan); + } + + @Override + public boolean onStartJob(JobParameters params) { + Log.v(TAG, "Starting job: " + String.valueOf(params)); + + if (NekoLand.DEBUG_NOTIFICATIONS) { + NotificationManager noman = getSystemService(NotificationManager.class); + final Bundle extras = new Bundle(); + extras.putString("android.substName", getString(R.string.notification_name)); + final int size = getResources() + .getDimensionPixelSize(android.R.dimen.notification_large_icon_width); + final Cat cat = Cat.create(this); + final Notification.Builder builder + = cat.buildNotification(this) + .setContentTitle("DEBUG") + .setChannelId(NekoLand.CHAN_ID) + .setContentText("Ran job: " + params); + + noman.notify(DEBUG_NOTIFICATION, builder.build()); + } + + triggerFoodResponse(this); + cancelJob(this); + return false; + } + + private static void triggerFoodResponse(Context context) { + final PrefState prefs = new PrefState(context); + int food = prefs.getFoodState(); + if (food != 0) { + prefs.setFoodState(0); // nom + final Random rng = new Random(); + if (rng.nextFloat() <= CAT_CAPTURE_PROB) { + Cat cat; + List<Cat> cats = prefs.getCats(); + final int[] probs = context.getResources().getIntArray(R.array.food_new_cat_prob); + final float waterLevel100 = prefs.getWaterState() / 2; // water is 0..200 + final float new_cat_prob = (float) ((food < probs.length) + ? probs[food] + : waterLevel100) / 100f; + Log.v(TAG, "Food type: " + food); + Log.v(TAG, "New cat probability: " + new_cat_prob); + + if (cats.size() == 0 || rng.nextFloat() <= new_cat_prob) { + cat = newRandomCat(context, prefs); + Log.v(TAG, "A new cat is here: " + cat.getName()); + } else { + cat = getExistingCat(prefs); + Log.v(TAG, "A cat has returned: " + cat.getName()); + } + + notifyCat(context, cat); + } + } + } + + static void notifyCat(Context context, Cat cat) { + NotificationManager noman = context.getSystemService(NotificationManager.class); + final Notification.Builder builder = cat.buildNotification(context); + noman.notify(cat.getShortcutId(), CAT_NOTIFICATION, builder.build()); + } + + static Cat newRandomCat(Context context, PrefState prefs) { + final Cat cat = Cat.create(context); + prefs.addCat(cat); + cat.logAdd(context); + return cat; + } + + static Cat getExistingCat(PrefState prefs) { + final List<Cat> cats = prefs.getCats(); + if (cats.size() == 0) return null; + return cats.get(new Random().nextInt(cats.size())); + } + + @Override + public boolean onStopJob(JobParameters jobParameters) { + return false; + } + + public static void registerJobIfNeeded(Context context, long intervalMinutes) { + JobScheduler jss = context.getSystemService(JobScheduler.class); + JobInfo info = jss.getPendingJob(JOB_ID); + if (info == null) { + registerJob(context, intervalMinutes); + } + } + + public static void registerJob(Context context, long intervalMinutes) { + setupNotificationChannels(context); + + JobScheduler jss = context.getSystemService(JobScheduler.class); + jss.cancel(JOB_ID); + long interval = intervalMinutes * MINUTES; + long jitter = (long) (INTERVAL_JITTER_FRAC * interval); + interval += (long) (Math.random() * (2 * jitter)) - jitter; + final JobInfo jobInfo = new JobInfo.Builder(JOB_ID, + new ComponentName(context, NekoService.class)) + .setPeriodic(interval, INTERVAL_FLEX) + .build(); + + Log.v(TAG, "A cat will visit in " + interval + "ms: " + String.valueOf(jobInfo)); + jss.schedule(jobInfo); + + if (NekoLand.DEBUG_NOTIFICATIONS) { + NotificationManager noman = context.getSystemService(NotificationManager.class); + noman.notify(DEBUG_NOTIFICATION, new Notification.Builder(context) + .setSmallIcon(R.drawable.stat_icon) + .setContentTitle(String.format("Job scheduled in %d min", (interval / MINUTES))) + .setContentText(String.valueOf(jobInfo)) + .setPriority(Notification.PRIORITY_MIN) + .setCategory(Notification.CATEGORY_SERVICE) + .setChannelId(NekoLand.CHAN_ID) + .setShowWhen(true) + .build()); + } + } + + public static void cancelJob(Context context) { + JobScheduler jss = context.getSystemService(JobScheduler.class); + Log.v(TAG, "Canceling job"); + jss.cancel(JOB_ID); + } +} diff --git a/packages/EasterEgg/src/com/android/egg/neko/NekoTile.java b/packages/EasterEgg/src/com/android/egg/neko/NekoTile.java new file mode 100644 index 000000000000..d02433f40e89 --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/neko/NekoTile.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2020 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.egg.neko; + +import android.content.Intent; +import android.service.quicksettings.Tile; +import android.service.quicksettings.TileService; +import android.util.Log; + +import com.android.egg.neko.PrefState.PrefsListener; +import com.android.internal.logging.MetricsLogger; + +public class NekoTile extends TileService implements PrefsListener { + + private static final String TAG = "NekoTile"; + + private PrefState mPrefs; + + @Override + public void onCreate() { + super.onCreate(); + mPrefs = new PrefState(this); + } + + @Override + public void onStartListening() { + super.onStartListening(); + mPrefs.setListener(this); + updateState(); + } + + @Override + public void onStopListening() { + super.onStopListening(); + mPrefs.setListener(null); + } + + @Override + public void onTileAdded() { + super.onTileAdded(); + MetricsLogger.count(this, "egg_neko_tile_added", 1); + } + + @Override + public void onTileRemoved() { + super.onTileRemoved(); + MetricsLogger.count(this, "egg_neko_tile_removed", 1); + } + + @Override + public void onPrefsChanged() { + updateState(); + } + + private void updateState() { + Tile tile = getQsTile(); + int foodState = mPrefs.getFoodState(); + Food food = new Food(foodState); + if (foodState != 0) { + NekoService.registerJobIfNeeded(this, food.getInterval(this)); + } + tile.setIcon(food.getIcon(this)); + tile.setLabel(food.getName(this)); + tile.setState(foodState != 0 ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE); + tile.updateTile(); + } + + @Override + public void onClick() { + if (mPrefs.getFoodState() != 0) { + // there's already food loaded, let's empty it + MetricsLogger.count(this, "egg_neko_empty_food", 1); + mPrefs.setFoodState(0); + NekoService.cancelJob(this); + } else { + // time to feed the cats + if (isLocked()) { + if (isSecure()) { + Log.d(TAG, "startActivityAndCollapse"); + Intent intent = new Intent(this, NekoLockedActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivityAndCollapse(intent); + } else { + unlockAndRun(new Runnable() { + @Override + public void run() { + showNekoDialog(); + } + }); + } + } else { + showNekoDialog(); + } + } + } + + private void showNekoDialog() { + Log.d(TAG, "showNekoDialog"); + MetricsLogger.count(this, "egg_neko_select_food", 1); + showDialog(new NekoDialog(this)); + } +} diff --git a/packages/EasterEgg/src/com/android/egg/neko/PrefState.java b/packages/EasterEgg/src/com/android/egg/neko/PrefState.java new file mode 100644 index 000000000000..49ff315392b4 --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/neko/PrefState.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2020 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.egg.neko; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class PrefState implements OnSharedPreferenceChangeListener { + + private static final String FILE_NAME = "mPrefs"; + + private static final String FOOD_STATE = "food"; + + private static final String WATER_STATE = "water"; + + private static final String CAT_KEY_PREFIX = "cat:"; + + private final Context mContext; + private final SharedPreferences mPrefs; + private PrefsListener mListener; + + public PrefState(Context context) { + mContext = context; + mPrefs = mContext.getSharedPreferences(FILE_NAME, 0); + } + + // Can also be used for renaming. + public void addCat(Cat cat) { + mPrefs.edit() + .putString(CAT_KEY_PREFIX + String.valueOf(cat.getSeed()), cat.getName()) + .apply(); + } + + public void removeCat(Cat cat) { + mPrefs.edit().remove(CAT_KEY_PREFIX + String.valueOf(cat.getSeed())).apply(); + } + + public List<Cat> getCats() { + ArrayList<Cat> cats = new ArrayList<>(); + Map<String, ?> map = mPrefs.getAll(); + for (String key : map.keySet()) { + if (key.startsWith(CAT_KEY_PREFIX)) { + long seed = Long.parseLong(key.substring(CAT_KEY_PREFIX.length())); + Cat cat = new Cat(mContext, seed); + cat.setName(String.valueOf(map.get(key))); + cats.add(cat); + } + } + return cats; + } + + public int getFoodState() { + return mPrefs.getInt(FOOD_STATE, 0); + } + + public void setFoodState(int foodState) { + mPrefs.edit().putInt(FOOD_STATE, foodState).apply(); + } + + public float getWaterState() { + return mPrefs.getFloat(WATER_STATE, 0f); + } + + public void setWaterState(float waterState) { + mPrefs.edit().putFloat(WATER_STATE, waterState).apply(); + } + + public void setListener(PrefsListener listener) { + mListener = listener; + if (mListener != null) { + mPrefs.registerOnSharedPreferenceChangeListener(this); + } else { + mPrefs.unregisterOnSharedPreferenceChangeListener(this); + } + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + mListener.onPrefsChanged(); + } + + public interface PrefsListener { + void onPrefsChanged(); + } +} diff --git a/packages/SettingsLib/AdaptiveIcon/res/values-night/colors.xml b/packages/SettingsLib/AdaptiveIcon/res/values-night/colors.xml new file mode 100644 index 000000000000..f985c1e2bd42 --- /dev/null +++ b/packages/SettingsLib/AdaptiveIcon/res/values-night/colors.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2020 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. +--> + +<resources> + <color name="advanced_outline_color">#FFFFFFFF</color> <!-- icon outline color --> +</resources> diff --git a/packages/SettingsLib/HelpUtils/res/values-en-rAU/strings.xml b/packages/SettingsLib/HelpUtils/res/values-en-rAU/strings.xml index 759da1d0b021..150020cb17c5 100644 --- a/packages/SettingsLib/HelpUtils/res/values-en-rAU/strings.xml +++ b/packages/SettingsLib/HelpUtils/res/values-en-rAU/strings.xml @@ -17,5 +17,5 @@ <resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <string name="help_feedback_label" msgid="7106780063063027882">"Help & feedback"</string> + <string name="help_feedback_label" msgid="7106780063063027882">"Help and feedback"</string> </resources> diff --git a/packages/SettingsLib/HelpUtils/res/values-en-rIN/strings.xml b/packages/SettingsLib/HelpUtils/res/values-en-rIN/strings.xml index 759da1d0b021..150020cb17c5 100644 --- a/packages/SettingsLib/HelpUtils/res/values-en-rIN/strings.xml +++ b/packages/SettingsLib/HelpUtils/res/values-en-rIN/strings.xml @@ -17,5 +17,5 @@ <resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <string name="help_feedback_label" msgid="7106780063063027882">"Help & feedback"</string> + <string name="help_feedback_label" msgid="7106780063063027882">"Help and feedback"</string> </resources> diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 5b6155180e0a..a7ef5e6f58f0 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -329,6 +329,7 @@ <activity android:name=".screenrecord.ScreenRecordDialog" android:theme="@style/ScreenRecord" + android:showForAllUsers="true" android:excludeFromRecents="true" /> <service android:name=".screenrecord.RecordingService" /> diff --git a/packages/SystemUI/res/layout/controls_management.xml b/packages/SystemUI/res/layout/controls_management.xml index 46f79deff109..ae7f44d19430 100644 --- a/packages/SystemUI/res/layout/controls_management.xml +++ b/packages/SystemUI/res/layout/controls_management.xml @@ -69,7 +69,7 @@ android:layout_width="wrap_content" android:layout_height="match_parent" android:gravity="center_vertical" - android:text="See other apps" + android:text="@string/controls_favorite_see_other_apps" style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 0e3fa1e48695..db45a60ab7c0 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2744,6 +2744,9 @@ <!-- Controls management favorites screen, See other apps with changes made [CHAR LIMIT=NONE] --> <string name="controls_favorite_toast_no_changes">Changes not saved</string> + <!-- Controls management favorites screen. See other apps button [CHAR LIMIT=30] --> + <string name="controls_favorite_see_other_apps">See other apps</string> + <!-- Controls management controls screen error on load message [CHAR LIMIT=NONE] --> <string name="controls_favorite_load_error">Controls could not be loaded. Check the <xliff:g id="app" example="System UI">%s</xliff:g> app to make sure that the app settings haven\u2019t changed.</string> <!-- Controls management controls screen no controls found on load message [CHAR LIMIT=NONE] --> diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java index a8bbdf6b68c9..6dc8322a5cf3 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java @@ -71,6 +71,7 @@ class Bubble implements BubbleViewProvider { // Items that are typically loaded later private String mAppName; private ShortcutInfo mShortcutInfo; + private String mMetadataShortcutId; private BadgedImageView mIconView; private BubbleExpandedView mExpandedView; @@ -136,6 +137,7 @@ class Bubble implements BubbleViewProvider { final int desiredHeight, final int desiredHeightResId, @Nullable final String title) { Objects.requireNonNull(key); Objects.requireNonNull(shortcutInfo); + mMetadataShortcutId = shortcutInfo.getId(); mShortcutInfo = shortcutInfo; mKey = key; mFlags = 0; @@ -218,6 +220,14 @@ class Bubble implements BubbleViewProvider { return mTitle; } + String getMetadataShortcutId() { + return mMetadataShortcutId; + } + + boolean hasMetadataShortcutId() { + return (mMetadataShortcutId != null && !mMetadataShortcutId.isEmpty()); + } + /** * Call when the views should be removed, ensure this is called to clean up ActivityView * content. @@ -350,9 +360,9 @@ class Bubble implements BubbleViewProvider { mAppUid = entry.getSbn().getUid(); mInstanceId = entry.getSbn().getInstanceId(); mFlyoutMessage = BubbleViewInfoTask.extractFlyoutMessage(entry); - mShortcutInfo = (entry.getBubbleMetadata() != null - && entry.getBubbleMetadata().getShortcutId() != null - && entry.getRanking() != null) ? entry.getRanking().getShortcutInfo() : null; + mShortcutInfo = (entry.getRanking() != null ? entry.getRanking().getShortcutInfo() : null); + mMetadataShortcutId = (entry.getBubbleMetadata() != null + ? entry.getBubbleMetadata().getShortcutId() : null); if (entry.getRanking() != null) { mIsVisuallyInterruptive = entry.getRanking().visuallyInterruptive(); } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java index aa417191b204..b739999fb652 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java @@ -207,6 +207,12 @@ public class BubbleController implements ConfigurationController.ConfigurationLi /** Whether or not the BubbleStackView has been added to the WindowManager. */ private boolean mAddedToWindowManager = false; + /** + * Value from {@link NotificationShadeWindowController#getForceHasTopUi()} when we forced top UI + * due to expansion. We'll restore this value when the stack collapses. + */ + private boolean mHadTopUi = false; + // Listens to user switch so bubbles can be saved and restored. private final NotificationLockscreenUserManager mNotifUserManager; @@ -483,12 +489,13 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } /** - * Dispatches a back press into the expanded Bubble's ActivityView if its IME is visible, - * causing it to hide. + * Hides the current input method, wherever it may be focused, via InputMethodManagerInternal. */ - public void hideImeFromExpandedBubble() { - if (mStackView != null) { - mStackView.hideImeFromExpandedBubble(); + public void hideCurrentInputMethod() { + try { + mBarService.hideCurrentInputMethodForBubbles(); + } catch (RemoteException e) { + e.printStackTrace(); } } @@ -693,8 +700,8 @@ public class BubbleController implements ConfigurationController.ConfigurationLi if (mStackView == null) { mStackView = new BubbleStackView( mContext, mBubbleData, mSurfaceSynchronizer, mFloatingContentCoordinator, - mSysUiState, this::onAllBubblesAnimatedOut, - this::onImeVisibilityChanged); + mSysUiState, this::onAllBubblesAnimatedOut, this::onImeVisibilityChanged, + this::hideCurrentInputMethod); mStackView.addView(mBubbleScrim); if (mExpandListener != null) { mStackView.setExpandListener(mExpandListener); @@ -1290,6 +1297,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi // Collapsing? Do this first before remaining steps. if (update.expandedChanged && !update.expanded) { mStackView.setExpanded(false); + mNotificationShadeWindowController.setForceHasTopUi(mHadTopUi); } // Do removals, if any. @@ -1376,6 +1384,8 @@ public class BubbleController implements ConfigurationController.ConfigurationLi if (update.expandedChanged && update.expanded) { if (mStackView != null) { mStackView.setExpanded(true); + mHadTopUi = mNotificationShadeWindowController.getForceHasTopUi(); + mNotificationShadeWindowController.setForceHasTopUi(true); } } @@ -1589,7 +1599,11 @@ public class BubbleController implements ConfigurationController.ConfigurationLi @Override public void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) { if (mStackView != null && taskInfo.displayId == getExpandedDisplayId(mContext)) { - mBubbleData.setExpanded(false); + if (mImeVisible) { + hideCurrentInputMethod(); + } else { + mBubbleData.setExpanded(false); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java index 7020f1cb88eb..c170ee271e1d 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java @@ -366,11 +366,19 @@ public class BubbleData { validShortcutIds.add(info.getId()); } - final Predicate<Bubble> invalidBubblesFromPackage = bubble -> - packageName.equals(bubble.getPackageName()) - && (bubble.getShortcutInfo() == null - || !bubble.getShortcutInfo().isEnabled() - || !validShortcutIds.contains(bubble.getShortcutInfo().getId())); + final Predicate<Bubble> invalidBubblesFromPackage = bubble -> { + final boolean bubbleIsFromPackage = packageName.equals(bubble.getPackageName()); + final boolean isShortcutBubble = bubble.hasMetadataShortcutId(); + if (!bubbleIsFromPackage || !isShortcutBubble) { + return false; + } + final boolean hasShortcutIdAndValidShortcut = + bubble.hasMetadataShortcutId() + && bubble.getShortcutInfo() != null + && bubble.getShortcutInfo().isEnabled() + && validShortcutIds.contains(bubble.getShortcutInfo().getId()); + return bubbleIsFromPackage && !hasShortcutIdAndValidShortcut; + }; final Consumer<Bubble> removeBubble = bubble -> dismissBubbleWithKey(bubble.getKey(), reason); diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDataRepository.kt b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDataRepository.kt index 390f7064a227..db64a13f3df3 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDataRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDataRepository.kt @@ -77,7 +77,7 @@ internal class BubbleDataRepository @Inject constructor( BubbleEntity( userId, b.packageName, - b.shortcutInfo?.id ?: return@mapNotNull null, + b.metadataShortcutId ?: return@mapNotNull null, b.key, b.rawDesiredHeight, b.rawDesiredHeightResId, diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java index 7c3e027e2e65..2bfe015c2787 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java @@ -173,7 +173,8 @@ public class BubbleExpandedView extends LinearLayout { return; } try { - if (!mIsOverflow && mBubble.getShortcutInfo() != null) { + if (!mIsOverflow && mBubble.hasMetadataShortcutId() + && mBubble.getShortcutInfo() != null) { options.setApplyActivityFlagsForBubbles(true); mActivityView.startShortcutActivity(mBubble.getShortcutInfo(), options, null /* sourceBounds */); @@ -465,7 +466,6 @@ public class BubbleExpandedView extends LinearLayout { @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); - hideImeIfVisible(); mKeyboardVisible = false; mNeedsNewHeight = false; if (mActivityView != null) { @@ -506,6 +506,14 @@ public class BubbleExpandedView extends LinearLayout { } } + @Nullable ActivityView getActivityView() { + return mActivityView; + } + + int getTaskId() { + return mTaskId; + } + /** * Called by {@link BubbleStackView} when the insets for the expanded state should be updated. * This should be done post-move and post-animation. @@ -616,7 +624,7 @@ public class BubbleExpandedView extends LinearLayout { if (isNew) { mPendingIntent = mBubble.getBubbleIntent(); - if (mPendingIntent != null || mBubble.getShortcutInfo() != null) { + if (mPendingIntent != null || mBubble.hasMetadataShortcutId()) { setContentVisibility(false); mActivityView.setVisibility(VISIBLE); } @@ -788,7 +796,7 @@ public class BubbleExpandedView extends LinearLayout { } private boolean usingActivityView() { - return (mPendingIntent != null || mBubble.getShortcutInfo() != null) + return (mPendingIntent != null || mBubble.hasMetadataShortcutId()) && mActivityView != null; } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java index a888bd57c699..ffb650d62064 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java @@ -70,9 +70,6 @@ public class BubbleExperimentConfig { private static final String WHITELISTED_AUTO_BUBBLE_APPS = "whitelisted_auto_bubble_apps"; - private static final String ALLOW_BUBBLE_OVERFLOW = "allow_bubble_overflow"; - private static final boolean ALLOW_BUBBLE_OVERFLOW_DEFAULT = true; - /** * When true, if a notification has the information necessary to bubble (i.e. valid * contentIntent and an icon or image), then a {@link android.app.Notification.BubbleMetadata} @@ -87,15 +84,6 @@ public class BubbleExperimentConfig { } /** - * When true, show a menu with dismissed and aged-out bubbles. - */ - static boolean allowBubbleOverflow(Context context) { - return Settings.Secure.getInt(context.getContentResolver(), - ALLOW_BUBBLE_OVERFLOW, - ALLOW_BUBBLE_OVERFLOW_DEFAULT ? 1 : 0) != 0; - } - - /** * Same as {@link #allowAnyNotifToBubble(Context)} except it filters for notifications that * are using {@link Notification.MessagingStyle} and have remote input. */ diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java index 50828e878154..1f3d981b8c9f 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java @@ -31,6 +31,7 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.annotation.SuppressLint; +import android.app.ActivityView; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; @@ -293,11 +294,42 @@ public class BubbleStackView extends FrameLayout /** Description of current animation controller state. */ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("Stack view state:"); - pw.print(" gestureInProgress: "); pw.println(mIsGestureInProgress); - pw.print(" showingDismiss: "); pw.println(mShowingDismiss); - pw.print(" isExpansionAnimating: "); pw.println(mIsExpansionAnimating); + pw.print(" gestureInProgress: "); pw.println(mIsGestureInProgress); + pw.print(" showingDismiss: "); pw.println(mShowingDismiss); + pw.print(" isExpansionAnimating: "); pw.println(mIsExpansionAnimating); + pw.print(" expandedContainerVis: "); pw.println(mExpandedViewContainer.getVisibility()); + pw.print(" expandedContainerAlpha: "); pw.println(mExpandedViewContainer.getAlpha()); + pw.print(" expandedContainerMatrix: "); + pw.println(mExpandedViewContainer.getAnimationMatrix()); + mStackAnimationController.dump(fd, pw, args); mExpandedAnimationController.dump(fd, pw, args); + + if (mExpandedBubble != null) { + pw.println("Expanded bubble state:"); + pw.println(" expandedBubbleKey: " + mExpandedBubble.getKey()); + + final BubbleExpandedView expandedView = mExpandedBubble.getExpandedView(); + + if (expandedView != null) { + pw.println(" expandedViewVis: " + expandedView.getVisibility()); + pw.println(" expandedViewAlpha: " + expandedView.getAlpha()); + pw.println(" expandedViewTaskId: " + expandedView.getTaskId()); + + final ActivityView av = expandedView.getActivityView(); + + if (av != null) { + pw.println(" activityViewVis: " + av.getVisibility()); + pw.println(" activityViewAlpha: " + av.getAlpha()); + } else { + pw.println(" activityView is null"); + } + } else { + pw.println("Expanded bubble view state: expanded bubble view is null"); + } + } else { + pw.println("Expanded bubble state: expanded bubble is null"); + } } private BubbleController.BubbleExpandListener mExpandListener; @@ -384,6 +416,11 @@ public class BubbleStackView extends FrameLayout public final Consumer<Boolean> mOnImeVisibilityChanged; /** + * Callback to run to ask BubbleController to hide the current IME. + */ + private final Runnable mHideCurrentInputMethodCallback; + + /** * The currently magnetized object, which is being dragged and will be attracted to the magnetic * dismiss target. * @@ -560,7 +597,7 @@ public class BubbleStackView extends FrameLayout mMagneticTarget, mIndividualBubbleMagnetListener); - hideImeFromExpandedBubble(); + hideCurrentInputMethod(); // Save the magnetized individual bubble so we can dispatch touch events to it. mMagnetizedObject = mExpandedAnimationController.getMagnetizedBubbleDraggingOut(); @@ -732,7 +769,8 @@ public class BubbleStackView extends FrameLayout FloatingContentCoordinator floatingContentCoordinator, SysUiState sysUiState, Runnable allBubblesAnimatedOutAction, - Consumer<Boolean> onImeVisibilityChanged) { + Consumer<Boolean> onImeVisibilityChanged, + Runnable hideCurrentInputMethodCallback) { super(context); mBubbleData = data; @@ -868,6 +906,7 @@ public class BubbleStackView extends FrameLayout setUpOverflow(); mOnImeVisibilityChanged = onImeVisibilityChanged; + mHideCurrentInputMethodCallback = hideCurrentInputMethodCallback; setOnApplyWindowInsetsListener((View view, WindowInsets insets) -> { onImeVisibilityChanged.accept(insets.getInsets(WindowInsets.Type.ime()).bottom > 0); @@ -1145,9 +1184,6 @@ public class BubbleStackView extends FrameLayout } private void setUpOverflow() { - if (!BubbleExperimentConfig.allowBubbleOverflow(mContext)) { - return; - } int overflowBtnIndex = 0; if (mBubbleOverflow == null) { mBubbleOverflow = new BubbleOverflow(getContext()); @@ -1513,8 +1549,7 @@ public class BubbleStackView extends FrameLayout } private void updateOverflowVisibility() { - if (!BubbleExperimentConfig.allowBubbleOverflow(mContext) - || mBubbleOverflow == null) { + if (mBubbleOverflow == null) { return; } mBubbleOverflow.setVisible(mIsExpanded ? VISIBLE : GONE); @@ -1593,6 +1628,8 @@ public class BubbleStackView extends FrameLayout updatePointerPosition(); if (mIsExpanded) { + hideCurrentInputMethod(); + // Make the container of the expanded view transparent before removing the expanded view // from it. Otherwise a punch hole created by {@link android.view.SurfaceView} in the // expanded view becomes visible on the screen. See b/126856255 @@ -1601,11 +1638,6 @@ public class BubbleStackView extends FrameLayout if (previouslySelected != null) { previouslySelected.setContentVisibility(false); } - if (previouslySelected != null && previouslySelected.getExpandedView() != null) { - // Hide the currently expanded bubble's IME if it's visible before switching - // to a new bubble. - previouslySelected.getExpandedView().hideImeIfVisible(); - } updateExpandedBubble(); requestUpdate(); @@ -1633,6 +1665,8 @@ public class BubbleStackView extends FrameLayout return; } + hideCurrentInputMethod(); + mSysUiState .setFlag(QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED, shouldExpand) .commitUpdate(mContext.getDisplayId()); @@ -1816,12 +1850,12 @@ public class BubbleStackView extends FrameLayout } } - void hideImeFromExpandedBubble() { - if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { - // Hide the currently expanded bubble's IME if it's visible before switching to a new - // bubble. - mExpandedBubble.getExpandedView().hideImeIfVisible(); - } + /** + * Asks the BubbleController to hide the IME from anywhere, whether it's focused on Bubbles or + * not. + */ + void hideCurrentInputMethod() { + mHideCurrentInputMethodCallback.run(); } private void beforeExpandedViewAnimation() { @@ -1898,7 +1932,7 @@ public class BubbleStackView extends FrameLayout AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), mScaleInSpringConfig) .addUpdateListener((target, values) -> { - if (mExpandedBubble.getIconView() == null) { + if (mExpandedBubble == null || mExpandedBubble.getIconView() == null) { return; } mExpandedViewContainerMatrix.postTranslate( @@ -1938,10 +1972,6 @@ public class BubbleStackView extends FrameLayout mAnimatingOutSurfaceContainer.setScaleX(0f); mAnimatingOutSurfaceContainer.setScaleY(0f); - if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { - mExpandedBubble.getExpandedView().hideImeIfVisible(); - } - // Let the expanded animation controller know that it shouldn't animate child adds/reorders // since we're about to animate collapsed. mExpandedAnimationController.notifyPreparingToCollapse(); @@ -2776,11 +2806,8 @@ public class BubbleStackView extends FrameLayout * @return the number of bubbles in the stack view. */ public int getBubbleCount() { - if (BubbleExperimentConfig.allowBubbleOverflow(mContext)) { - // Subtract 1 for the overflow button that is always in the bubble container. - return mBubbleContainer.getChildCount() - 1; - } - return mBubbleContainer.getChildCount(); + // Subtract 1 for the overflow button that is always in the bubble container. + return mBubbleContainer.getChildCount() - 1; } /** diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/AnimatableScaleMatrix.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/AnimatableScaleMatrix.java index ae7833634794..07acb710c6d7 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/AnimatableScaleMatrix.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/AnimatableScaleMatrix.java @@ -134,4 +134,11 @@ public class AnimatableScaleMatrix extends Matrix { public float getPivotY() { return mPivotY; } + + @Override + public boolean equals(Object obj) { + // Use object equality to allow this matrix to be used as a map key (which is required for + // PhysicsAnimator's animator caching). + return obj == this; + } } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java index 98a7cc23c67e..6e6f82b714ff 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java @@ -441,7 +441,10 @@ public class PhysicsAnimationLayout extends FrameLayout { // Cancel physics animations on the view. for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) { - getAnimationFromView(property, view).cancel(); + final DynamicAnimation animationFromView = getAnimationFromView(property, view); + if (animationFromView != null) { + animationFromView.cancel(); + } } } @@ -499,13 +502,13 @@ public class PhysicsAnimationLayout extends FrameLayout { * Retrieves the animation of the given property from the view at the given index via the view * tag system. */ - private SpringAnimation getAnimationAtIndex( + @Nullable private SpringAnimation getAnimationAtIndex( DynamicAnimation.ViewProperty property, int index) { return getAnimationFromView(property, getChildAt(index)); } /** Retrieves the animation of the given property from the view via the view tag system. */ - private SpringAnimation getAnimationFromView( + @Nullable private SpringAnimation getAnimationFromView( DynamicAnimation.ViewProperty property, View view) { return (SpringAnimation) view.getTag(getTagIdForProperty(property)); } @@ -536,8 +539,10 @@ public class PhysicsAnimationLayout extends FrameLayout { final float offset = mController.getOffsetForChainedPropertyAnimation(property); if (nextAnimInChain < getChildCount()) { - getAnimationAtIndex(property, nextAnimInChain) - .animateToFinalPosition(value + offset); + final SpringAnimation nextAnim = getAnimationAtIndex(property, nextAnimInChain); + if (nextAnim != null) { + nextAnim.animateToFinalPosition(value + offset); + } } }); diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt index e2215d57a094..68625059b2ef 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt @@ -2,6 +2,7 @@ package com.android.systemui.media import android.content.Context import android.content.Intent +import android.content.res.Configuration import android.graphics.Color import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS import android.view.LayoutInflater @@ -96,7 +97,6 @@ class MediaCarouselController @Inject constructor( * The measured height of the carousel */ private var carouselMeasureHeight: Int = 0 - private var playerWidthPlusPadding: Int = 0 private var desiredHostState: MediaHostState? = null private val mediaCarousel: MediaScrollView private val mediaCarouselScrollHandler: MediaCarouselScrollHandler @@ -108,6 +108,15 @@ class MediaCarouselController @Inject constructor( private val pageIndicator: PageIndicator private val visualStabilityCallback: VisualStabilityManager.Callback private var needsReordering: Boolean = false + private var isRtl: Boolean = false + set(value) { + if (value != field) { + field = value + mediaFrame.layoutDirection = + if (value) View.LAYOUT_DIRECTION_RTL else View.LAYOUT_DIRECTION_LTR + mediaCarouselScrollHandler.scrollToStart() + } + } private var currentlyExpanded = true set(value) { if (field != value) { @@ -126,6 +135,11 @@ class MediaCarouselController @Inject constructor( override fun onOverlayChanged() { inflateSettingsButton() } + + override fun onConfigChanged(newConfig: Configuration?) { + if (newConfig == null) return + isRtl = newConfig.layoutDirection == View.LAYOUT_DIRECTION_RTL + } } init { @@ -135,6 +149,7 @@ class MediaCarouselController @Inject constructor( mediaCarouselScrollHandler = MediaCarouselScrollHandler(mediaCarousel, pageIndicator, executor, mediaDataManager::onSwipeToDismiss, this::updatePageIndicatorLocation, falsingManager) + isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL inflateSettingsButton() mediaContent = mediaCarousel.requireViewById(R.id.media_carousel) configurationController.addCallback(configListener) @@ -144,7 +159,7 @@ class MediaCarouselController @Inject constructor( reorderAllPlayers() } // Let's reset our scroll position - mediaCarousel.scrollX = 0 + mediaCarouselScrollHandler.scrollToStart() } visualStabilityManager.addReorderingAllowedCallback(visualStabilityCallback, true /* persistent */) @@ -196,8 +211,13 @@ class MediaCarouselController @Inject constructor( } private fun inflateMediaCarousel(): ViewGroup { - return LayoutInflater.from(context).inflate(R.layout.media_carousel, + val mediaCarousel = LayoutInflater.from(context).inflate(R.layout.media_carousel, UniqueObjectHostView(context), false) as ViewGroup + // Because this is inflated when not attached to the true view hierarchy, it resolves some + // potential issues to force that the layout direction is defined by the locale + // (rather than inherited from the parent, which would resolve to LTR when unattached). + mediaCarousel.layoutDirection = View.LAYOUT_DIRECTION_LOCALE + return mediaCarousel } private fun reorderAllPlayers() { @@ -313,8 +333,12 @@ class MediaCarouselController @Inject constructor( private fun updatePageIndicatorLocation() { // Update the location of the page indicator, carousel clipping - pageIndicator.translationX = (currentCarouselWidth - pageIndicator.width) / 2.0f + - mediaCarouselScrollHandler.contentTranslation + val translationX = if (isRtl) { + (pageIndicator.width - currentCarouselWidth) / 2.0f + } else { + (currentCarouselWidth - pageIndicator.width) / 2.0f + } + pageIndicator.translationX = translationX + mediaCarouselScrollHandler.contentTranslation val layoutParams = pageIndicator.layoutParams as ViewGroup.MarginLayoutParams pageIndicator.translationY = (currentCarouselHeight - pageIndicator.height - layoutParams.bottomMargin).toFloat() @@ -334,7 +358,8 @@ class MediaCarouselController @Inject constructor( if (width != currentCarouselWidth || height != currentCarouselHeight) { currentCarouselWidth = width currentCarouselHeight = height - mediaCarouselScrollHandler.setCarouselBounds(currentCarouselWidth, currentCarouselHeight) + mediaCarouselScrollHandler.setCarouselBounds( + currentCarouselWidth, currentCarouselHeight) updatePageIndicatorLocation() } } @@ -348,7 +373,7 @@ class MediaCarouselController @Inject constructor( if (currentlyShowingOnlyActive != endShowsActive || ((currentTransitionProgress != 1.0f && currentTransitionProgress != 0.0f) && startShowsActive != endShowsActive)) { - /// Whenever we're transitioning from between differing states or the endstate differs + // Whenever we're transitioning from between differing states or the endstate differs // we reset the translation currentlyShowingOnlyActive = endShowsActive mediaCarouselScrollHandler.resetTranslation(animate = true) @@ -416,14 +441,15 @@ class MediaCarouselController @Inject constructor( height != carouselMeasureWidth && height != 0) { carouselMeasureWidth = width carouselMeasureHeight = height - playerWidthPlusPadding = carouselMeasureWidth + context.resources.getDimensionPixelSize( - R.dimen.qs_media_padding) - mediaCarouselScrollHandler.playerWidthPlusPadding = playerWidthPlusPadding + val playerWidthPlusPadding = carouselMeasureWidth + + context.resources.getDimensionPixelSize(R.dimen.qs_media_padding) // Let's remeasure the carousel val widthSpec = desiredHostState?.measurementInput?.widthMeasureSpec ?: 0 val heightSpec = desiredHostState?.measurementInput?.heightMeasureSpec ?: 0 mediaCarousel.measure(widthSpec, heightSpec) mediaCarousel.layout(0, 0, width, mediaCarousel.measuredHeight) + // Update the padding after layout; view widths are used in RTL to calculate scrollX + mediaCarouselScrollHandler.playerWidthPlusPadding = playerWidthPlusPadding } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt index 993c05fbbd6f..ef2f71100e1a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt @@ -59,6 +59,10 @@ class MediaCarouselScrollHandler( private val falsingManager: FalsingManager ) { /** + * Is the view in RTL + */ + val isRtl: Boolean get() = scrollView.isLayoutRtl + /** * Do we need falsing protection? */ var falsingProtectionNeeded: Boolean = false @@ -121,14 +125,14 @@ class MediaCarouselScrollHandler( field = value // The player width has changed, let's update the scroll position to make sure // it's still at the same place - var newScroll = activeMediaIndex * playerWidthPlusPadding + var newRelativeScroll = activeMediaIndex * playerWidthPlusPadding if (scrollIntoCurrentMedia > playerWidthPlusPadding) { - newScroll += playerWidthPlusPadding - + newRelativeScroll += playerWidthPlusPadding - (scrollIntoCurrentMedia - playerWidthPlusPadding) } else { - newScroll += scrollIntoCurrentMedia + newRelativeScroll += scrollIntoCurrentMedia } - scrollView.scrollX = newScroll + scrollView.relativeScrollX = newRelativeScroll } /** @@ -184,8 +188,9 @@ class MediaCarouselScrollHandler( if (playerWidthPlusPadding == 0) { return } - onMediaScrollingChanged(scrollX / playerWidthPlusPadding, - scrollX % playerWidthPlusPadding) + val relativeScrollX = scrollView.relativeScrollX + onMediaScrollingChanged(relativeScrollX / playerWidthPlusPadding, + relativeScrollX % playerWidthPlusPadding) } } @@ -222,11 +227,19 @@ class MediaCarouselScrollHandler( Math.abs(contentTranslation)) val settingsTranslation = (1.0f - settingsOffset) * -settingsButton.width * SETTINGS_BUTTON_TRANSLATION_FRACTION - val newTranslationX: Float - if (contentTranslation > 0) { - newTranslationX = settingsTranslation + val newTranslationX = if (isRtl) { + // In RTL, the 0-placement is on the right side of the view, not the left... + if (contentTranslation > 0) { + -(scrollView.width - settingsTranslation - settingsButton.width) + } else { + -settingsTranslation + } } else { - newTranslationX = scrollView.width - settingsTranslation - settingsButton.width + if (contentTranslation > 0) { + settingsTranslation + } else { + scrollView.width - settingsTranslation - settingsButton.width + } } val rotation = (1.0f - settingsOffset) * 50 settingsButton.rotation = rotation * -Math.signum(contentTranslation) @@ -259,26 +272,26 @@ class MediaCarouselScrollHandler( } if (isUp || motionEvent.action == MotionEvent.ACTION_CANCEL) { // It's an up and the fling didn't take it above - val pos = scrollView.scrollX % playerWidthPlusPadding - val scollXAmount: Int - if (pos > playerWidthPlusPadding / 2) { - scollXAmount = playerWidthPlusPadding - pos + val relativePos = scrollView.relativeScrollX % playerWidthPlusPadding + val scrollXAmount: Int + if (relativePos > playerWidthPlusPadding / 2) { + scrollXAmount = playerWidthPlusPadding - relativePos } else { - scollXAmount = -1 * pos + scrollXAmount = -1 * relativePos } - if (scollXAmount != 0) { + if (scrollXAmount != 0) { // Delay the scrolling since scrollView calls springback which cancels // the animation again.. mainExecutor.execute { - scrollView.smoothScrollBy(scollXAmount, 0) + scrollView.smoothScrollBy(if (isRtl) -scrollXAmount else scrollXAmount, 0) } } val currentTranslation = scrollView.getContentTranslation() if (currentTranslation != 0.0f) { // We started a Swipe but didn't end up with a fling. Let's either go to the // dismissed position or go back. - val springBack = Math.abs(currentTranslation) < getMaxTranslation() / 2 - || isFalseTouch() + val springBack = Math.abs(currentTranslation) < getMaxTranslation() / 2 || + isFalseTouch() val newTranslation: Float if (springBack) { newTranslation = 0.0f @@ -313,9 +326,11 @@ class MediaCarouselScrollHandler( return gestureDetector.onTouchEvent(motionEvent) } - fun onScroll(down: MotionEvent, - lastMotion: MotionEvent, - distanceX: Float): Boolean { + fun onScroll( + down: MotionEvent, + lastMotion: MotionEvent, + distanceX: Float + ): Boolean { val totalX = lastMotion.x - down.x val currentTranslation = scrollView.getContentTranslation() if (currentTranslation != 0.0f || @@ -339,8 +354,8 @@ class MediaCarouselScrollHandler( } // Otherwise we don't have do do anything, and will remove the unrubberbanded // translation } - if (Math.signum(newTranslation) != Math.signum(currentTranslation) - && currentTranslation != 0.0f) { + if (Math.signum(newTranslation) != Math.signum(currentTranslation) && + currentTranslation != 0.0f) { // We crossed the 0.0 threshold of the translation. Let's see if we're allowed // to scroll into the new direction if (scrollView.canScrollHorizontally(-newTranslation.toInt())) { @@ -394,9 +409,10 @@ class MediaCarouselScrollHandler( scrollView.animationTargetX = newTranslation } else { // We're flinging the player! Let's go either to the previous or to the next player - val pos = scrollView.scrollX + val pos = scrollView.relativeScrollX val currentIndex = if (playerWidthPlusPadding > 0) pos / playerWidthPlusPadding else 0 - var destIndex = if (vX <= 0) currentIndex + 1 else currentIndex + val flungTowardEnd = if (isRtl) vX > 0 else vX < 0 + var destIndex = if (flungTowardEnd) currentIndex + 1 else currentIndex destIndex = Math.max(0, destIndex) destIndex = Math.min(mediaContent.getChildCount() - 1, destIndex) val view = mediaContent.getChildAt(destIndex) @@ -438,8 +454,14 @@ class MediaCarouselScrollHandler( activeMediaIndex = newIndex updatePlayerVisibilities() } - val location = activeMediaIndex.toFloat() + if (playerWidthPlusPadding > 0) + val relativeLocation = activeMediaIndex.toFloat() + if (playerWidthPlusPadding > 0) scrollInAmount.toFloat() / playerWidthPlusPadding else 0f + // Fix the location, because PageIndicator does not handle RTL internally + val location = if (isRtl) { + mediaContent.childCount - relativeLocation - 1 + } else { + relativeLocation + } pageIndicator.setLocation(location) updateClipToOutline() } @@ -480,13 +502,20 @@ class MediaCarouselScrollHandler( * where it was and update our scroll position. */ fun onPrePlayerRemoved(removed: MediaControlPanel) { - val beforeActive = mediaContent.indexOfChild(removed.view?.player) <= activeMediaIndex + val removedIndex = mediaContent.indexOfChild(removed.view?.player) + // If the removed index is less than the activeMediaIndex, then we need to decrement it. + // RTL has no effect on this, because indices are always relative (start-to-end). + // Update the index 'manually' since we won't always get a call to onMediaScrollingChanged + val beforeActive = removedIndex <= activeMediaIndex if (beforeActive) { - // also update the index here since the scroll below might not always lead - // to a scrolling changed activeMediaIndex = Math.max(0, activeMediaIndex - 1) - scrollView.scrollX = Math.max(scrollView.scrollX - - playerWidthPlusPadding, 0) + } + // If the removed media item is "left of" the active one (in an absolute sense), we need to + // scroll the view to keep that player in view. This is because scroll position is always + // calculated from left to right. + val leftOfActive = if (isRtl) !beforeActive else beforeActive + if (leftOfActive) { + scrollView.scrollX = Math.max(scrollView.scrollX - playerWidthPlusPadding, 0) } } @@ -501,6 +530,13 @@ class MediaCarouselScrollHandler( } } + /** + * Reset the MediaScrollView to the start. + */ + fun scrollToStart() { + scrollView.relativeScrollX = 0 + } + companion object { private val CONTENT_TRANSLATION = object : FloatPropertyCompat<MediaCarouselScrollHandler>( "contentTranslation") { diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt index 416c81aee263..5052386e65e1 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt @@ -432,12 +432,13 @@ class MediaDataManager( } } - val resumeAction: Runnable? = mediaEntries.get(key)?.resumeAction - val hasCheckedForResume = mediaEntries.get(key)?.hasCheckedForResume == true foregroundExecutor.execute { + val resumeAction: Runnable? = mediaEntries[key]?.resumeAction + val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true + val active = mediaEntries[key]?.active ?: true onMediaDataLoaded(key, oldKey, MediaData(true, bgColor, app, smallIconDrawable, artist, song, artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token, - notif.contentIntent, null, active = true, resumeAction = resumeAction, + notif.contentIntent, null, active, resumeAction = resumeAction, notificationKey = key, hasCheckedForResume = hasCheckedForResume)) } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt b/packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt index a079b06a0b10..b8872250bb6c 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt @@ -15,7 +15,10 @@ import com.android.systemui.util.animation.physicsAnimator * when only measuring children but not the parent, when trying to apply a new scroll position */ class MediaScrollView @JvmOverloads constructor( - context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : HorizontalScrollView(context, attrs, defStyleAttr) { lateinit var contentContainer: ViewGroup @@ -38,6 +41,26 @@ class MediaScrollView @JvmOverloads constructor( } /** + * Convert between the absolute (left-to-right) and relative (start-to-end) scrollX of the media + * carousel. The player indices are always relative (start-to-end) and the scrollView.scrollX + * is always absolute. This function is its own inverse. + */ + private fun transformScrollX(scrollX: Int): Int = if (isLayoutRtl) { + contentContainer.width - width - scrollX + } else { + scrollX + } + + /** + * Get the layoutDirection-relative (start-to-end) scroll X position of the carousel. + */ + var relativeScrollX: Int + get() = transformScrollX(scrollX) + set(value) { + scrollX = transformScrollX(value) + } + + /** * Allow all scrolls to go through, use base implementation */ override fun scrollTo(x: Int, y: Int) { @@ -55,15 +78,15 @@ class MediaScrollView @JvmOverloads constructor( } override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { - var intercept = false; + var intercept = false touchListener?.let { intercept = it.onInterceptTouchEvent(ev) } - return super.onInterceptTouchEvent(ev) || intercept; + return super.onInterceptTouchEvent(ev) || intercept } override fun onTouchEvent(ev: MotionEvent?): Boolean { - var touch = false; + var touch = false touchListener?.let { touch = it.onTouchEvent(ev) } @@ -75,9 +98,17 @@ class MediaScrollView @JvmOverloads constructor( contentContainer = getChildAt(0) as ViewGroup } - override fun overScrollBy(deltaX: Int, deltaY: Int, scrollX: Int, scrollY: Int, - scrollRangeX: Int, scrollRangeY: Int, maxOverScrollX: Int, - maxOverScrollY: Int, isTouchEvent: Boolean): Boolean { + override fun overScrollBy( + deltaX: Int, + deltaY: Int, + scrollX: Int, + scrollY: Int, + scrollRangeX: Int, + scrollRangeY: Int, + maxOverScrollX: Int, + maxOverScrollY: Int, + isTouchEvent: Boolean + ): Boolean { if (getContentTranslation() != 0.0f) { // When we're dismissing we ignore all the scrolling return false diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt index cf8a636a2b67..9a134dbe0264 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt @@ -71,6 +71,7 @@ class MediaTimeoutListener @Inject constructor( ) : MediaController.Callback() { var timedOut = false + private var playing: Boolean? = null // Resume controls may have null token private val mediaController = if (data.token != null) { @@ -94,7 +95,13 @@ class MediaTimeoutListener @Inject constructor( Log.v(TAG, "onPlaybackStateChanged: $state") } - if (state == null || !isPlayingState(state.state)) { + val isPlaying = state != null && isPlayingState(state.state) + if (playing == isPlaying && playing != null) { + return + } + playing = isPlaying + + if (!isPlaying) { if (DEBUG) { Log.v(TAG, "schedule timeout for $key") } diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java b/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java index d92434678357..e2feb71735ff 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java +++ b/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java @@ -273,8 +273,7 @@ public class PipTaskOrganizer extends TaskOrganizer implements sendOnPipTransitionStarted(direction); // Don't bother doing an animation if the display rotation differs or if it's in // a non-supported windowing mode - wct.setWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); - wct.setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); + applyWindowingModeChangeOnExit(wct, direction); WindowOrganizer.applyTransaction(wct); // Send finished callback though animation is ignored. sendOnPipTransitionFinished(direction); @@ -303,6 +302,16 @@ public class PipTaskOrganizer extends TaskOrganizer implements mExitingPip = true; } + private void applyWindowingModeChangeOnExit(WindowContainerTransaction wct, int direction) { + // Reset the final windowing mode. + wct.setWindowingMode(mToken, getOutPipWindowingMode()); + // Simply reset the activity mode set prior to the animation running. + wct.setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); + if (mSplitDivider != null && direction == TRANSITION_DIRECTION_TO_SPLIT_SCREEN) { + wct.reparent(mToken, mSplitDivider.getSecondaryRoot(), true /* onTop */); + } + } + /** * Removes PiP immediately. */ @@ -745,13 +754,7 @@ public class PipTaskOrganizer extends TaskOrganizer implements // on the task to ensure that the task "matches" the parent's bounds. taskBounds = (direction == TRANSITION_DIRECTION_TO_FULLSCREEN) ? null : destinationBounds; - // Reset the final windowing mode. - wct.setWindowingMode(mToken, getOutPipWindowingMode()); - // Simply reset the activity mode set prior to the animation running. - wct.setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); - if (mSplitDivider != null && direction == TRANSITION_DIRECTION_TO_SPLIT_SCREEN) { - wct.reparent(mToken, mSplitDivider.getSecondaryRoot(), true /* onTop */); - } + applyWindowingModeChangeOnExit(wct, direction); } else { // Just a resize in PIP taskBounds = destinationBounds; diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMotionHelper.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMotionHelper.java index 26805050e841..e60123e863a4 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMotionHelper.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMotionHelper.java @@ -365,6 +365,10 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, void flingToSnapTarget( float velocityX, float velocityY, @Nullable Runnable updateAction, @Nullable Runnable endAction) { + // If we're flinging to a snap target now, we're not springing to catch up to the touch + // location now. + mSpringingToTouch = false; + mTemporaryBoundsPhysicsAnimator .spring(FloatProperties.RECT_WIDTH, mBounds.width(), mSpringConfig) .spring(FloatProperties.RECT_HEIGHT, mBounds.height(), mSpringConfig) diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java index e66b33c660d6..9dcc924f161e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java @@ -64,6 +64,8 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha private TouchAnimator mTranslationYAnimator; private TouchAnimator mNonfirstPageAnimator; private TouchAnimator mNonfirstPageDelayedAnimator; + // This animates fading of SecurityFooter and media divider + private TouchAnimator mAllPagesDelayedAnimator; private TouchAnimator mBrightnessAnimator; private boolean mNeedsAnimatorUpdate = false; @@ -296,19 +298,24 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha Builder builder = new Builder() .setStartDelay(EXPANDED_TILE_DELAY) .addFloat(tileLayout, "alpha", 0, 1); + mFirstPageDelayedAnimator = builder.build(); + + // Fade in the security footer and the divider as we reach the final position + builder = new Builder().setStartDelay(EXPANDED_TILE_DELAY); if (mQsPanel.getSecurityFooter() != null) { builder.addFloat(mQsPanel.getSecurityFooter().getView(), "alpha", 0, 1); } if (mQsPanel.getDivider() != null) { builder.addFloat(mQsPanel.getDivider(), "alpha", 0, 1); } - mFirstPageDelayedAnimator = builder.build(); + mAllPagesDelayedAnimator = builder.build(); if (mQsPanel.getSecurityFooter() != null) { mAllViews.add(mQsPanel.getSecurityFooter().getView()); } if (mQsPanel.getDivider() != null) { mAllViews.add(mQsPanel.getDivider()); } + float px = 0; float py = 1; if (tiles.size() <= 3) { @@ -388,6 +395,9 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha mNonfirstPageAnimator.setPosition(position); mNonfirstPageDelayedAnimator.setPosition(position); } + if (mAllowFancy) { + mAllPagesDelayedAnimator.setPosition(position); + } } @Override diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java index 87faaccf2063..795d0627c447 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java @@ -14,8 +14,8 @@ package com.android.systemui.qs.tileimpl; -import static androidx.lifecycle.Lifecycle.State.DESTROYED; import static androidx.lifecycle.Lifecycle.State.RESUMED; +import static androidx.lifecycle.Lifecycle.State.STARTED; import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_CLICK; import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_LONG_PRESS; @@ -432,17 +432,19 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy } private void handleSetListeningInternal(Object listener, boolean listening) { + // This should be used to go from resumed to paused. Listening for ON_RESUME and ON_PAUSE + // in this lifecycle will determine the listening window. if (listening) { if (mListeners.add(listener) && mListeners.size() == 1) { if (DEBUG) Log.d(TAG, "handleSetListening true"); - mLifecycle.markState(RESUMED); + mLifecycle.setCurrentState(RESUMED); handleSetListening(listening); refreshState(); // Ensure we get at least one refresh after listening. } } else { if (mListeners.remove(listener) && mListeners.size() == 0) { if (DEBUG) Log.d(TAG, "handleSetListening false"); - mLifecycle.markState(DESTROYED); + mLifecycle.setCurrentState(STARTED); handleSetListening(listening); } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java index 32ef063a55be..0c34b27d348e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java @@ -22,13 +22,13 @@ import android.text.TextUtils; import android.util.Log; import android.widget.Switch; -import com.android.internal.logging.UiEventLogger; import com.android.systemui.R; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.qs.QSHost; import com.android.systemui.qs.tileimpl.QSTileImpl; import com.android.systemui.screenrecord.RecordingController; +import com.android.systemui.statusbar.phone.KeyguardDismissUtil; import javax.inject.Inject; @@ -39,19 +39,17 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState> implements RecordingController.RecordingStateChangeCallback { private static final String TAG = "ScreenRecordTile"; private RecordingController mController; - private ActivityStarter mActivityStarter; + private KeyguardDismissUtil mKeyguardDismissUtil; private long mMillisUntilFinished = 0; private Callback mCallback = new Callback(); - private UiEventLogger mUiEventLogger; @Inject public ScreenRecordTile(QSHost host, RecordingController controller, - ActivityStarter activityStarter, UiEventLogger uiEventLogger) { + KeyguardDismissUtil keyguardDismissUtil) { super(host); mController = controller; mController.observe(this, mCallback); - mActivityStarter = activityStarter; - mUiEventLogger = uiEventLogger; + mKeyguardDismissUtil = keyguardDismissUtil; } @Override @@ -69,7 +67,7 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState> } else if (mController.isRecording()) { stopRecording(); } else { - startCountdown(); + mUiHandler.post(() -> showPrompt()); } refreshState(); } @@ -114,11 +112,15 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState> return mContext.getString(R.string.quick_settings_screen_record_label); } - private void startCountdown() { - // Close QS, otherwise the permission dialog appears beneath it + private void showPrompt() { + // Close QS, otherwise the dialog appears beneath it getHost().collapsePanels(); Intent intent = mController.getPromptIntent(); - mActivityStarter.postStartActivityDismissingKeyguard(intent, 0); + ActivityStarter.OnDismissAction dismissAction = () -> { + mContext.startActivity(intent); + return false; + }; + mKeyguardDismissUtil.executeWhenUnlocked(dismissAction, false); } private void cancelCountdown() { diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java index b253635e9bfa..82ac1f6f6a33 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java @@ -17,12 +17,17 @@ package com.android.systemui.screenrecord; import android.app.PendingIntent; +import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.os.CountDownTimer; +import android.os.UserHandle; import android.util.Log; +import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.statusbar.policy.CallbackController; import java.util.ArrayList; @@ -41,21 +46,30 @@ public class RecordingController private static final String SYSUI_SCREENRECORD_LAUNCHER = "com.android.systemui.screenrecord.ScreenRecordDialog"; - private final Context mContext; private boolean mIsStarting; private boolean mIsRecording; private PendingIntent mStopIntent; private CountDownTimer mCountDownTimer = null; + private BroadcastDispatcher mBroadcastDispatcher; private ArrayList<RecordingStateChangeCallback> mListeners = new ArrayList<>(); + @VisibleForTesting + protected final BroadcastReceiver mUserChangeReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (mStopIntent != null) { + stopRecording(); + } + } + }; + /** * Create a new RecordingController - * @param context Context for the controller */ @Inject - public RecordingController(Context context) { - mContext = context; + public RecordingController(BroadcastDispatcher broadcastDispatcher) { + mBroadcastDispatcher = broadcastDispatcher; } /** @@ -99,6 +113,9 @@ public class RecordingController } try { startIntent.send(); + IntentFilter userFilter = new IntentFilter(Intent.ACTION_USER_SWITCHED); + mBroadcastDispatcher.registerReceiver(mUserChangeReceiver, userFilter, null, + UserHandle.ALL); Log.d(TAG, "sent start intent"); } catch (PendingIntent.CanceledException e) { Log.e(TAG, "Pending intent was cancelled: " + e.getMessage()); @@ -146,11 +163,16 @@ public class RecordingController */ public void stopRecording() { try { - mStopIntent.send(); + if (mStopIntent != null) { + mStopIntent.send(); + } else { + Log.e(TAG, "Stop intent was null"); + } updateState(false); } catch (PendingIntent.CanceledException e) { Log.e(TAG, "Error stopping: " + e.getMessage()); } + mBroadcastDispatcher.unregisterReceiver(mUserChangeReceiver); } /** diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java index 87597263168a..476ec798a35f 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java @@ -32,6 +32,7 @@ import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; +import android.os.UserHandle; import android.provider.Settings; import android.util.Log; import android.widget.Toast; @@ -40,6 +41,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.UiEventLogger; import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.LongRunning; +import com.android.systemui.settings.CurrentUserContextTracker; import java.io.IOException; import java.util.concurrent.Executor; @@ -58,7 +60,6 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis private static final String TAG = "RecordingService"; private static final String CHANNEL_ID = "screen_record"; private static final String EXTRA_RESULT_CODE = "extra_resultCode"; - private static final String EXTRA_DATA = "extra_data"; private static final String EXTRA_PATH = "extra_path"; private static final String EXTRA_AUDIO_SOURCE = "extra_useAudio"; private static final String EXTRA_SHOW_TAPS = "extra_showTaps"; @@ -79,14 +80,17 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis private final Executor mLongExecutor; private final UiEventLogger mUiEventLogger; private final NotificationManager mNotificationManager; + private final CurrentUserContextTracker mUserContextTracker; @Inject public RecordingService(RecordingController controller, @LongRunning Executor executor, - UiEventLogger uiEventLogger, NotificationManager notificationManager) { + UiEventLogger uiEventLogger, NotificationManager notificationManager, + CurrentUserContextTracker userContextTracker) { mController = controller; mLongExecutor = executor; mUiEventLogger = uiEventLogger; mNotificationManager = notificationManager; + mUserContextTracker = userContextTracker; } /** @@ -95,8 +99,6 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis * @param context Context from the requesting activity * @param resultCode The result code from {@link android.app.Activity#onActivityResult(int, int, * android.content.Intent)} - * @param data The data from {@link android.app.Activity#onActivityResult(int, int, - * android.content.Intent)} * @param audioSource The ordinal value of the audio source * {@link com.android.systemui.screenrecord.ScreenRecordingAudioSource} * @param showTaps True to make touches visible while recording @@ -118,6 +120,8 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis String action = intent.getAction(); Log.d(TAG, "onStartCommand " + action); + int mCurrentUserId = mUserContextTracker.getCurrentUserContext().getUserId(); + UserHandle currentUser = new UserHandle(mCurrentUserId); switch (action) { case ACTION_START: mAudioSource = ScreenRecordingAudioSource @@ -132,8 +136,8 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis setTapsVisible(mShowTaps); mRecorder = new ScreenMediaRecorder( - getApplicationContext(), - getUserId(), + mUserContextTracker.getCurrentUserContext(), + mCurrentUserId, mAudioSource, this ); @@ -148,7 +152,14 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis } else { mUiEventLogger.log(Events.ScreenRecordEvent.SCREEN_RECORD_END_QS_TILE); } - stopRecording(); + // Check user ID - we may be getting a stop intent after user switch, in which case + // we want to post the notifications for that user, which is NOT current user + int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); + if (userId == -1) { + userId = mUserContextTracker.getCurrentUserContext().getUserId(); + } + Log.d(TAG, "notifying for user " + userId); + stopRecording(userId); mNotificationManager.cancel(NOTIFICATION_RECORDING_ID); stopSelf(); break; @@ -165,7 +176,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); // Remove notification - mNotificationManager.cancel(NOTIFICATION_VIEW_ID); + mNotificationManager.cancelAsUser(null, NOTIFICATION_VIEW_ID, currentUser); startActivity(Intent.createChooser(shareIntent, shareLabel) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); @@ -184,7 +195,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis Toast.LENGTH_LONG).show(); // Remove notification - mNotificationManager.cancel(NOTIFICATION_VIEW_ID); + mNotificationManager.cancelAsUser(null, NOTIFICATION_VIEW_ID, currentUser); Log.d(TAG, "Deleted recording " + uri); break; } @@ -215,11 +226,12 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis mController.updateState(true); createRecordingNotification(); mUiEventLogger.log(Events.ScreenRecordEvent.SCREEN_RECORD_START); - } catch (IOException | RemoteException e) { + } catch (IOException | RemoteException | IllegalStateException e) { Toast.makeText(this, R.string.screenrecord_start_error, Toast.LENGTH_LONG) .show(); e.printStackTrace(); + mController.updateState(false); } } @@ -242,7 +254,6 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis ? res.getString(R.string.screenrecord_ongoing_screen_only) : res.getString(R.string.screenrecord_ongoing_screen_and_audio); - Intent stopIntent = getNotificationIntent(this); Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID) .setSmallIcon(R.drawable.ic_screenrecord) @@ -254,7 +265,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis .setOngoing(true) .setContentIntent( PendingIntent.getService(this, REQUEST_CODE, stopIntent, - PendingIntent.FLAG_UPDATE_CURRENT)) + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)) .addExtras(extras); startForeground(NOTIFICATION_RECORDING_ID, builder.build()); } @@ -265,11 +276,17 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis String notificationTitle = mAudioSource == ScreenRecordingAudioSource.NONE ? res.getString(R.string.screenrecord_ongoing_screen_only) : res.getString(R.string.screenrecord_ongoing_screen_and_audio); + + Bundle extras = new Bundle(); + extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, + res.getString(R.string.screenrecord_name)); + Notification.Builder builder = new Notification.Builder(getApplicationContext(), CHANNEL_ID) .setContentTitle(notificationTitle) .setContentText( getResources().getString(R.string.screenrecord_background_processing_label)) - .setSmallIcon(R.drawable.ic_screenrecord); + .setSmallIcon(R.drawable.ic_screenrecord) + .addExtras(extras); return builder.build(); } @@ -287,7 +304,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis this, REQUEST_CODE, getShareIntent(this, uri.toString()), - PendingIntent.FLAG_UPDATE_CURRENT)) + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)) .build(); Notification.Action deleteAction = new Notification.Action.Builder( @@ -297,7 +314,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis this, REQUEST_CODE, getDeleteIntent(this, uri.toString()), - PendingIntent.FLAG_UPDATE_CURRENT)) + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)) .build(); Bundle extras = new Bundle(); @@ -328,34 +345,36 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis return builder.build(); } - private void stopRecording() { + private void stopRecording(int userId) { setTapsVisible(mOriginalShowTaps); if (getRecorder() != null) { getRecorder().end(); - saveRecording(); + saveRecording(userId); } else { Log.e(TAG, "stopRecording called, but recorder was null"); } mController.updateState(false); } - private void saveRecording() { - mNotificationManager.notify(NOTIFICATION_PROCESSING_ID, createProcessingNotification()); + private void saveRecording(int userId) { + UserHandle currentUser = new UserHandle(userId); + mNotificationManager.notifyAsUser(null, NOTIFICATION_PROCESSING_ID, + createProcessingNotification(), currentUser); mLongExecutor.execute(() -> { try { Log.d(TAG, "saving recording"); Notification notification = createSaveNotification(getRecorder().save()); if (!mController.isRecording()) { - Log.d(TAG, "showing saved notification"); - mNotificationManager.notify(NOTIFICATION_VIEW_ID, notification); + mNotificationManager.notifyAsUser(null, NOTIFICATION_VIEW_ID, notification, + currentUser); } } catch (IOException e) { Log.e(TAG, "Error saving screen recording: " + e.getMessage()); Toast.makeText(this, R.string.screenrecord_delete_error, Toast.LENGTH_LONG) .show(); } finally { - mNotificationManager.cancel(NOTIFICATION_PROCESSING_ID); + mNotificationManager.cancelAsUser(null, NOTIFICATION_PROCESSING_ID, currentUser); } }); } @@ -371,7 +390,9 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis * @return */ public static Intent getStopIntent(Context context) { - return new Intent(context, RecordingService.class).setAction(ACTION_STOP); + return new Intent(context, RecordingService.class) + .setAction(ACTION_STOP) + .putExtra(Intent.EXTRA_USER_HANDLE, context.getUserId()); } /** diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenInternalAudioRecorder.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenInternalAudioRecorder.java index edbc3cfdece5..df03c3e08f08 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenInternalAudioRecorder.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenInternalAudioRecorder.java @@ -16,7 +16,6 @@ package com.android.systemui.screenrecord; -import android.content.Context; import android.media.AudioAttributes; import android.media.AudioFormat; import android.media.AudioPlaybackCaptureConfiguration; @@ -39,7 +38,6 @@ public class ScreenInternalAudioRecorder { private static String TAG = "ScreenAudioRecorder"; private static final int TIMEOUT = 500; private static final float MIC_VOLUME_SCALE = 1.4f; - private final Context mContext; private AudioRecord mAudioRecord; private AudioRecord mAudioRecordMic; private Config mConfig = new Config(); @@ -49,17 +47,14 @@ public class ScreenInternalAudioRecorder { private long mPresentationTime; private long mTotalBytes; private MediaMuxer mMuxer; - private String mOutFile; private boolean mMic; private int mTrackId = -1; - public ScreenInternalAudioRecorder(String outFile, Context context, - MediaProjection mp, boolean includeMicInput) throws IOException { + public ScreenInternalAudioRecorder(String outFile, MediaProjection mp, boolean includeMicInput) + throws IOException { mMic = includeMicInput; - mOutFile = outFile; mMuxer = new MediaMuxer(outFile, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); - mContext = context; mMediaProjection = mp; Log.d(TAG, "creating audio file " + outFile); setupSimple(); @@ -266,8 +261,9 @@ public class ScreenInternalAudioRecorder { /** * start recording + * @throws IllegalStateException if recording fails to initialize */ - public void start() { + public void start() throws IllegalStateException { if (mThread != null) { Log.e(TAG, "a recording is being done in parallel or stop is not called"); } @@ -276,8 +272,7 @@ public class ScreenInternalAudioRecorder { Log.d(TAG, "channel count " + mAudioRecord.getChannelCount()); mCodec.start(); if (mAudioRecord.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) { - Log.e(TAG, "Error starting audio recording"); - return; + throw new IllegalStateException("Audio recording failed to start"); } mThread.start(); } diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java index 1c7d987afff2..1a9abb9cf27d 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java @@ -166,7 +166,7 @@ public class ScreenMediaRecorder { mAudioSource == MIC_AND_INTERNAL) { mTempAudioFile = File.createTempFile("temp", ".aac", mContext.getCacheDir()); - mAudio = new ScreenInternalAudioRecorder(mTempAudioFile.getAbsolutePath(), mContext, + mAudio = new ScreenInternalAudioRecorder(mTempAudioFile.getAbsolutePath(), mMediaProjection, mAudioSource == MIC_AND_INTERNAL); } @@ -175,7 +175,7 @@ public class ScreenMediaRecorder { /** * Start screen recording */ - void start() throws IOException, RemoteException { + void start() throws IOException, RemoteException, IllegalStateException { Log.d(TAG, "start recording"); prepare(); mMediaRecorder.start(); @@ -205,7 +205,7 @@ public class ScreenMediaRecorder { } } - private void recordInternalAudio() { + private void recordInternalAudio() throws IllegalStateException { if (mAudioSource == INTERNAL || mAudioSource == MIC_AND_INTERNAL) { mAudio.start(); } diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java index 8347def2d430..dc47ab4dff63 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java @@ -23,16 +23,19 @@ import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.NONE; import android.app.Activity; import android.app.PendingIntent; +import android.content.Context; import android.os.Bundle; import android.view.Gravity; import android.view.ViewGroup; import android.view.Window; +import android.view.WindowManager; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.Spinner; import android.widget.Switch; import com.android.systemui.R; +import com.android.systemui.settings.CurrentUserContextTracker; import java.util.ArrayList; import java.util.List; @@ -48,16 +51,17 @@ public class ScreenRecordDialog extends Activity { private static final String TAG = "ScreenRecordDialog"; private final RecordingController mController; + private final CurrentUserContextTracker mCurrentUserContextTracker; private Switch mTapsSwitch; private Switch mAudioSwitch; private Spinner mOptions; private List<ScreenRecordingAudioSource> mModes; - private int mSelected; - @Inject - public ScreenRecordDialog(RecordingController controller) { + public ScreenRecordDialog(RecordingController controller, + CurrentUserContextTracker currentUserContextTracker) { mController = controller; + mCurrentUserContextTracker = currentUserContextTracker; } @Override @@ -68,6 +72,7 @@ public class ScreenRecordDialog extends Activity { // Inflate the decor view, so the attributes below are not overwritten by the theme. window.getDecorView(); window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + window.addPrivateFlags(WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS); window.setGravity(Gravity.TOP); setTitle(R.string.screenrecord_name); @@ -100,24 +105,24 @@ public class ScreenRecordDialog extends Activity { mOptions.setOnItemClickListenerInt((parent, view, position, id) -> { mAudioSwitch.setChecked(true); }); - } private void requestScreenCapture() { + Context userContext = mCurrentUserContextTracker.getCurrentUserContext(); boolean showTaps = mTapsSwitch.isChecked(); ScreenRecordingAudioSource audioMode = mAudioSwitch.isChecked() ? (ScreenRecordingAudioSource) mOptions.getSelectedItem() : NONE; - PendingIntent startIntent = PendingIntent.getForegroundService(this, + PendingIntent startIntent = PendingIntent.getForegroundService(userContext, RecordingService.REQUEST_CODE, RecordingService.getStartIntent( - ScreenRecordDialog.this, RESULT_OK, + userContext, RESULT_OK, audioMode.ordinal(), showTaps), - PendingIntent.FLAG_UPDATE_CURRENT); - PendingIntent stopIntent = PendingIntent.getService(this, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + PendingIntent stopIntent = PendingIntent.getService(userContext, RecordingService.REQUEST_CODE, - RecordingService.getStopIntent(this), - PendingIntent.FLAG_UPDATE_CURRENT); + RecordingService.getStopIntent(userContext), + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); mController.startCountdown(DELAY_MS, INTERVAL_MS, startIntent, stopIntent); } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java index 9bbc4ddcc62c..8e878ddc6da1 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java @@ -58,7 +58,6 @@ import android.os.Looper; import android.os.Message; import android.os.PowerManager; import android.os.RemoteException; -import android.os.UserHandle; import android.provider.Settings; import android.util.DisplayMetrics; import android.util.Log; @@ -1179,11 +1178,15 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset @Override public void onReceive(Context context, Intent intent) { PendingIntent pendingIntent = intent.getParcelableExtra(EXTRA_ACTION_INTENT); - Intent actionIntent = pendingIntent.getIntent(); String actionType = intent.getStringExtra(EXTRA_ACTION_TYPE); - Slog.d(TAG, "Executing smart action [" + actionType + "]:" + actionIntent); + Slog.d(TAG, "Executing smart action [" + actionType + "]:" + pendingIntent.getIntent()); ActivityOptions opts = ActivityOptions.makeBasic(); - context.startActivityAsUser(actionIntent, opts.toBundle(), UserHandle.CURRENT); + + try { + pendingIntent.send(context, 0, null, null, null, null, opts.toBundle()); + } catch (PendingIntent.CanceledException e) { + Log.e(TAG, "Pending intent canceled", e); + } ScreenshotSmartActions.notifyScreenshotAction( context, intent.getStringExtra(EXTRA_ID), actionType, true); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java index e3fbdbc7c30d..468b9b16addb 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java @@ -281,8 +281,10 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { Intent.createChooser(sharingIntent, null, chooserAction.getIntentSender()) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - PendingIntent pendingIntent = PendingIntent.getActivityAsUser(context, requestCode, - sharingChooserIntent, 0, null, UserHandle.CURRENT); + + // cancel current pending intent (if any) since clipData isn't used for matching + PendingIntent pendingIntent = PendingIntent.getActivityAsUser(context, 0, + sharingChooserIntent, PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT); // Create a share action for the notification PendingIntent shareAction = PendingIntent.getBroadcastAsUser(context, requestCode, diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/Divider.java b/packages/SystemUI/src/com/android/systemui/stackdivider/Divider.java index ad312202e224..8a3819925f30 100644 --- a/packages/SystemUI/src/com/android/systemui/stackdivider/Divider.java +++ b/packages/SystemUI/src/com/android/systemui/stackdivider/Divider.java @@ -253,7 +253,9 @@ public class Divider extends SystemUI implements DividerView.DividerCallbacks, mSplitLayout.mSecondary = new Rect(mRotateSplitLayout.mSecondary); mRotateSplitLayout = null; } - update(newConfig); + if (isSplitActive()) { + update(newConfig); + } } Handler getHandler() { @@ -328,11 +330,6 @@ public class Divider extends SystemUI implements DividerView.DividerCallbacks, mHandler.post(this::removeDivider); } - void onTasksReady() { - mHandler.post(() -> update(mDisplayController.getDisplayContext( - mContext.getDisplayId()).getResources().getConfiguration())); - } - private void updateVisibility(final boolean visible) { if (DEBUG) Slog.d(TAG, "Updating visibility " + mVisible + "->" + visible); if (mVisible != visible) { @@ -534,7 +531,7 @@ public class Divider extends SystemUI implements DividerView.DividerCallbacks, void ensureMinimizedSplit() { setHomeMinimized(true /* minimized */, mHomeStackResizable); - if (!isDividerVisible()) { + if (mView != null && !isDividerVisible()) { // Wasn't in split-mode yet, so enter now. if (DEBUG) { Slog.d(TAG, " entering split mode with minimized=true"); @@ -545,7 +542,7 @@ public class Divider extends SystemUI implements DividerView.DividerCallbacks, void ensureNormalSplit() { setHomeMinimized(false /* minimized */, mHomeStackResizable); - if (!isDividerVisible()) { + if (mView != null && !isDividerVisible()) { // Wasn't in split-mode, so enter now. if (DEBUG) { Slog.d(TAG, " enter split mode unminimized "); diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/SplitScreenTaskOrganizer.java b/packages/SystemUI/src/com/android/systemui/stackdivider/SplitScreenTaskOrganizer.java index 6751e8d6223d..4a2cad705c17 100644 --- a/packages/SystemUI/src/com/android/systemui/stackdivider/SplitScreenTaskOrganizer.java +++ b/packages/SystemUI/src/com/android/systemui/stackdivider/SplitScreenTaskOrganizer.java @@ -113,8 +113,6 @@ class SplitScreenTaskOrganizer extends TaskOrganizer { t.setColor(mSecondaryDim, new float[]{0f, 0f, 0f}); t.apply(); releaseTransaction(t); - - mDivider.onTasksReady(); } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java index 5926a5b86acd..304fe0090e77 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java @@ -121,6 +121,8 @@ public class EdgeBackGestureHandler extends CurrentUserTracker implements Displa private final Context mContext; private final OverviewProxyService mOverviewProxyService; + private final Runnable mStateChangeCallback; + private final PluginManager mPluginManager; // Activities which should not trigger Back gesture. private final List<ComponentName> mGestureBlockingActivities = new ArrayList<>(); @@ -196,13 +198,15 @@ public class EdgeBackGestureHandler extends CurrentUserTracker implements Displa }; public EdgeBackGestureHandler(Context context, OverviewProxyService overviewProxyService, - SysUiState sysUiFlagContainer, PluginManager pluginManager) { + SysUiState sysUiFlagContainer, PluginManager pluginManager, + Runnable stateChangeCallback) { super(Dependency.get(BroadcastDispatcher.class)); mContext = context; mDisplayId = context.getDisplayId(); mMainExecutor = context.getMainExecutor(); mOverviewProxyService = overviewProxyService; mPluginManager = pluginManager; + mStateChangeCallback = stateChangeCallback; ComponentName recentsComponentName = ComponentName.unflattenFromString( context.getString(com.android.internal.R.string.config_recentsComponentName)); if (recentsComponentName != null) { @@ -226,13 +230,13 @@ public class EdgeBackGestureHandler extends CurrentUserTracker implements Displa Log.e(TAG, "Failed to add gesture blocking activities", e); } } - Dependency.get(ProtoTracer.class).add(this); + Dependency.get(ProtoTracer.class).add(this); mLongPressTimeout = Math.min(MAX_LONG_PRESS_TIMEOUT, ViewConfiguration.getLongPressTimeout()); mGestureNavigationSettingsObserver = new GestureNavigationSettingsObserver( - mContext.getMainThreadHandler(), mContext, this::updateCurrentUserResources); + mContext.getMainThreadHandler(), mContext, this::onNavigationSettingsChanged); updateCurrentUserResources(); sysUiFlagContainer.addCallback(sysUiFlags -> mSysUiFlags = sysUiFlags); @@ -263,6 +267,14 @@ public class EdgeBackGestureHandler extends CurrentUserTracker implements Displa mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop() * backGestureSlop; } + private void onNavigationSettingsChanged() { + boolean wasBackAllowed = isHandlingGestures(); + updateCurrentUserResources(); + if (wasBackAllowed != isHandlingGestures()) { + mStateChangeCallback.run(); + } + } + @Override public void onUserSwitched(int newUserId) { updateIsEnabled(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java index 063305e0af90..27daf8615a31 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java @@ -25,6 +25,7 @@ import static android.view.InsetsState.ITYPE_NAVIGATION_BAR; import static android.view.InsetsState.containsType; import static android.view.WindowInsetsController.APPEARANCE_LOW_PROFILE_BARS; import static android.view.WindowInsetsController.APPEARANCE_OPAQUE_NAVIGATION_BARS; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; @@ -219,12 +220,13 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback * original handle hidden and we'll flip the visibilities once the * {@link #mTasksFrozenListener} fires */ - private VerticalNavigationHandle mOrientationHandle; + private QuickswitchOrientedNavHandle mOrientationHandle; private WindowManager.LayoutParams mOrientationParams; private int mStartingQuickSwitchRotation = -1; private int mCurrentRotation; private ViewTreeObserver.OnGlobalLayoutListener mOrientationHandleGlobalLayoutListener; private UiEventLogger mUiEventLogger; + private boolean mShowOrientedHandleForImmersiveMode; @com.android.internal.annotations.VisibleForTesting public enum NavBarActionEvent implements UiEventLogger.UiEventEnum { @@ -296,6 +298,9 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback @Override public void onQuickSwitchToNewTask(@Surface.Rotation int rotation) { mStartingQuickSwitchRotation = rotation; + if (rotation == -1) { + mShowOrientedHandleForImmersiveMode = false; + } orientSecondaryHomeHandle(); } @@ -585,7 +590,7 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback getContext().getSystemService(DisplayManager.class) .registerDisplayListener(this, new Handler(Looper.getMainLooper())); - mOrientationHandle = new VerticalNavigationHandle(getContext()); + mOrientationHandle = new QuickswitchOrientedNavHandle(getContext()); getBarTransitions().addDarkIntensityListener(mOrientationHandleIntensityListener); mOrientationParams = new WindowManager.LayoutParams(0, 0, @@ -596,8 +601,11 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH | WindowManager.LayoutParams.FLAG_SLIPPERY, PixelFormat.TRANSLUCENT); + mOrientationParams.setTitle("SecondaryHomeHandle" + getContext().getDisplayId()); + mOrientationParams.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION; mWindowManager.addView(mOrientationHandle, mOrientationParams); mOrientationHandle.setVisibility(View.GONE); + mOrientationParams.setFitInsetsTypes(0 /* types*/); mOrientationHandleGlobalLayoutListener = () -> { if (mStartingQuickSwitchRotation == -1) { @@ -634,22 +642,28 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback int height = 0; int width = 0; Rect dispSize = mWindowManager.getCurrentWindowMetrics().getBounds(); + mOrientationHandle.setDeltaRotation(deltaRotation); switch (deltaRotation) { case Surface.ROTATION_90: case Surface.ROTATION_270: height = dispSize.height(); - width = getResources() - .getDimensionPixelSize(R.dimen.navigation_bar_height); + width = mNavigationBarView.getHeight(); break; case Surface.ROTATION_180: case Surface.ROTATION_0: // TODO(b/152683657): Need to determine best UX for this - resetSecondaryHandle(); - return; + if (!mShowOrientedHandleForImmersiveMode) { + resetSecondaryHandle(); + return; + } + width = dispSize.width(); + height = mNavigationBarView.getHeight(); + break; } mOrientationParams.gravity = - deltaRotation == Surface.ROTATION_90 ? Gravity.LEFT : Gravity.RIGHT; + deltaRotation == Surface.ROTATION_0 ? Gravity.BOTTOM : + (deltaRotation == Surface.ROTATION_90 ? Gravity.LEFT : Gravity.RIGHT); mOrientationParams.height = height; mOrientationParams.width = width; mWindowManager.updateViewLayout(mOrientationHandle, mOrientationParams); @@ -743,6 +757,11 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback && mNavigationBarWindowState != state) { mNavigationBarWindowState = state; updateSystemUiStateFlags(-1); + mShowOrientedHandleForImmersiveMode = state == WINDOW_STATE_HIDDEN; + if (mOrientationHandle != null + && mStartingQuickSwitchRotation != -1) { + orientSecondaryHomeHandle(); + } if (DEBUG_WINDOW_STATE) Log.d(TAG, "Navigation bar " + windowStateToString(state)); if (mNavigationBarView != null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java index dbff643c6e32..1eab427b4155 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java @@ -326,8 +326,8 @@ public class NavigationBarView extends FrameLayout implements mNavColorSampleMargin = getResources() .getDimensionPixelSize(R.dimen.navigation_handle_sample_horizontal_margin); - mEdgeBackGestureHandler = new EdgeBackGestureHandler( - context, mOverviewProxyService, mSysUiFlagContainer, mPluginManager); + mEdgeBackGestureHandler = new EdgeBackGestureHandler(context, mOverviewProxyService, + mSysUiFlagContainer, mPluginManager, this::updateStates); mRegionSamplingHelper = new RegionSamplingHelper(this, new RegionSamplingHelper.SamplingCallback() { @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java index f2eec39ed17e..c32133119386 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java @@ -166,6 +166,7 @@ public class NotificationPanelViewController extends PanelViewController { private final ConfigurationListener mConfigurationListener = new ConfigurationListener(); private final StatusBarStateListener mStatusBarStateListener = new StatusBarStateListener(); private final ExpansionCallback mExpansionCallback = new ExpansionCallback(); + private final BiometricUnlockController mBiometricUnlockController; private final NotificationPanelView mView; private final MetricsLogger mMetricsLogger; private final ActivityManager mActivityManager; @@ -227,7 +228,8 @@ public class NotificationPanelViewController extends PanelViewController { mBarState == StatusBarState.KEYGUARD || mBarState == StatusBarState.SHADE_LOCKED; if (!running && mFirstBypassAttempt && keyguardOrShadeLocked && !mDozing - && !mDelayShowingKeyguardStatusBar) { + && !mDelayShowingKeyguardStatusBar + && !mBiometricUnlockController.isBiometricUnlock()) { mFirstBypassAttempt = false; animateKeyguardStatusBarIn(StackStateAnimator.ANIMATION_DURATION_STANDARD); } @@ -487,6 +489,7 @@ public class NotificationPanelViewController extends PanelViewController { StatusBarTouchableRegionManager statusBarTouchableRegionManager, ConversationNotificationManager conversationNotificationManager, MediaHierarchyManager mediaHierarchyManager, + BiometricUnlockController biometricUnlockController, StatusBarKeyguardViewManager statusBarKeyguardViewManager) { super(view, falsingManager, dozeLog, keyguardStateController, (SysuiStatusBarStateController) statusBarStateController, vibratorHelper, @@ -511,6 +514,7 @@ public class NotificationPanelViewController extends PanelViewController { mDisplayId = displayId; mPulseExpansionHandler = pulseExpansionHandler; mDozeParameters = dozeParameters; + mBiometricUnlockController = biometricUnlockController; pulseExpansionHandler.setPulseExpandAbortListener(() -> { if (mQs != null) { mQs.animateHeaderSlidingOut(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java index 06d35a36e3c4..5bb8fab8a62e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java @@ -688,8 +688,8 @@ public class PhoneStatusBarPolicy if (DEBUG) Log.d(TAG, "screenrecord: hiding icon during countdown"); mHandler.post(() -> mIconController.setIconVisibility(mSlotScreenRecord, false)); // Reset talkback priority - mIconController.setIconAccessibilityLiveRegion(mSlotScreenRecord, - View.ACCESSIBILITY_LIVE_REGION_NONE); + mHandler.post(() -> mIconController.setIconAccessibilityLiveRegion(mSlotScreenRecord, + View.ACCESSIBILITY_LIVE_REGION_NONE)); } @Override @@ -698,7 +698,7 @@ public class PhoneStatusBarPolicy mIconController.setIcon(mSlotScreenRecord, R.drawable.stat_sys_screen_record, mResources.getString(R.string.screenrecord_ongoing_screen_only)); - mIconController.setIconVisibility(mSlotScreenRecord, true); + mHandler.post(() -> mIconController.setIconVisibility(mSlotScreenRecord, true)); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/VerticalNavigationHandle.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickswitchOrientedNavHandle.java index 0cdf1d32d6a0..fe74677a8d51 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/VerticalNavigationHandle.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickswitchOrientedNavHandle.java @@ -19,19 +19,25 @@ package com.android.systemui.statusbar.phone; import android.content.Context; import android.graphics.Canvas; import android.graphics.RectF; +import android.view.Surface; import com.android.systemui.R; /** Temporarily shown view when using QuickSwitch to switch between apps of different rotations */ -public class VerticalNavigationHandle extends NavigationHandle { +public class QuickswitchOrientedNavHandle extends NavigationHandle { private final int mWidth; private final RectF mTmpBoundsRectF = new RectF(); + private @Surface.Rotation int mDeltaRotation; - public VerticalNavigationHandle(Context context) { + public QuickswitchOrientedNavHandle(Context context) { super(context); mWidth = context.getResources().getDimensionPixelSize(R.dimen.navigation_home_handle_width); } + void setDeltaRotation(@Surface.Rotation int rotation) { + mDeltaRotation = rotation; + } + @Override protected void onDraw(Canvas canvas) { canvas.drawRoundRect(computeHomeHandleBounds(), mRadius, mRadius, mPaint); @@ -42,12 +48,32 @@ public class VerticalNavigationHandle extends NavigationHandle { int top; int bottom; int right; - int topStart = getLocationOnScreen()[1]; int radiusOffset = mRadius * 2; - right = getWidth() - mBottom; - top = getHeight() / 2 - (mWidth / 2) - (topStart / 2); - left = getWidth() - mBottom - radiusOffset; - bottom = top + mWidth; + int topStart = getLocationOnScreen()[1]; + + switch (mDeltaRotation) { + default: + case Surface.ROTATION_0: + case Surface.ROTATION_180: + int height = mRadius * 2; + left = getWidth() / 2 - mWidth / 2; + top = (getHeight() - mBottom - height); + right = getWidth() / 2 + mWidth / 2; + bottom = top + height; + break; + case Surface.ROTATION_90: + left = mBottom; + right = left + radiusOffset; + top = getHeight() / 2 - (mWidth / 2) - (topStart / 2); + bottom = top + mWidth; + break; + case Surface.ROTATION_270: + right = getWidth() - mBottom; + left = right - radiusOffset; + top = getHeight() / 2 - (mWidth / 2) - (topStart / 2); + bottom = top + mWidth; + break; + } mTmpBoundsRectF.set(left, top, right, bottom); return mTmpBoundsRectF; } diff --git a/packages/SystemUI/src/com/android/systemui/util/sensors/ProximitySensor.java b/packages/SystemUI/src/com/android/systemui/util/sensors/ProximitySensor.java index 6794a2a3f72f..52d46476df83 100644 --- a/packages/SystemUI/src/com/android/systemui/util/sensors/ProximitySensor.java +++ b/packages/SystemUI/src/com/android/systemui/util/sensors/ProximitySensor.java @@ -253,7 +253,7 @@ public class ProximitySensor { private final AtomicBoolean mRegistered = new AtomicBoolean(); @Inject - public ProximityCheck(ProximitySensor sensor, DelayableExecutor delayableExecutor) { + public ProximityCheck(ProximitySensor sensor, @Main DelayableExecutor delayableExecutor) { mSensor = sensor; mSensor.setTag("prox_check"); mDelayableExecutor = delayableExecutor; diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsImeTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsImeTest.java index 8b254e3a2df0..58959c456cd9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsImeTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsImeTest.java @@ -18,6 +18,7 @@ package com.android.systemui.globalactions; import static android.view.WindowInsets.Type.ime; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -39,6 +40,7 @@ import androidx.test.rule.ActivityTestRule; import com.android.systemui.SysuiTestCase; +import org.junit.After; import org.junit.Rule; import org.junit.Test; @@ -52,6 +54,11 @@ public class GlobalActionsImeTest extends SysuiTestCase { public ActivityTestRule<TestActivity> mActivityTestRule = new ActivityTestRule<>( TestActivity.class, false, false); + @After + public void tearDown() { + executeShellCommand("input keyevent HOME"); + } + /** * This test verifies that GlobalActions, which is frequently used to capture bugreports, * doesn't interfere with the IME, i.e. soft-keyboard state. @@ -68,6 +75,9 @@ public class GlobalActionsImeTest extends SysuiTestCase { waitUntil("Ime is not visible", activity::isImeVisible); } + // In some cases, IME is not controllable. e.g., floating IME or fullscreen IME. + final boolean activityControlledIme = activity.mControlsIme; + executeShellCommand("input keyevent --longpress POWER"); waitUntil("activity loses focus", () -> !activity.mHasFocus); @@ -77,9 +87,9 @@ public class GlobalActionsImeTest extends SysuiTestCase { runAssertionOnMainThread(() -> { assertTrue("IME should remain visible behind GlobalActions, but didn't", - activity.mControlsIme); - assertTrue("App behind GlobalActions should remain in control of IME, but didn't", activity.mImeVisible); + assertEquals("App behind GlobalActions should remain in control of IME, but didn't", + activityControlledIme, activity.mControlsIme); }); } @@ -181,7 +191,7 @@ public class GlobalActionsImeTest extends SysuiTestCase { } boolean isImeVisible() { - return mHasFocus && mControlsIme && mImeVisible; + return mHasFocus && mImeVisible; } @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt index 20a6da548848..e56bbabfdc0b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt @@ -15,6 +15,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dump.DumpManager import com.android.systemui.statusbar.SbnBuilder import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.eq import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import org.junit.After @@ -26,8 +27,8 @@ import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.mock import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever import org.mockito.junit.MockitoJUnit +import org.mockito.Mockito.`when` as whenever private const val KEY = "KEY" private const val PACKAGE_NAME = "com.android.systemui" @@ -35,7 +36,6 @@ private const val APP_NAME = "SystemUI" private const val SESSION_ARTIST = "artist" private const val SESSION_TITLE = "title" -private fun <T> eq(value: T): T = Mockito.eq(value) ?: value private fun <T> anyObject(): T { return Mockito.anyObject<T>() } @@ -104,6 +104,19 @@ class MediaDataManagerTest : SysuiTestCase() { } @Test + fun testOnSwipeToDismiss_deactivatesMedia() { + val data = MediaData(initialized = true, backgroundColor = 0, app = null, appIcon = null, + artist = null, song = null, artwork = null, actions = emptyList(), + actionsToShowInCompact = emptyList(), packageName = "INVALID", token = null, + clickIntent = null, device = null, active = true, resumeAction = null) + mediaDataManager.onNotificationAdded(KEY, mediaNotification) + mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = data) + + mediaDataManager.onSwipeToDismiss() + assertThat(data.active).isFalse() + } + + @Test fun testLoadsMetadataOnBackground() { mediaDataManager.onNotificationAdded(KEY, mediaNotification) assertThat(backgroundExecutor.numPending()).isEqualTo(1) @@ -119,6 +132,30 @@ class MediaDataManagerTest : SysuiTestCase() { } @Test + fun testOnMetaDataLoaded_conservesActiveFlag() { + val listener = TestListener() + whenever(mediaControllerFactory.create(anyObject())).thenReturn(controller) + whenever(controller.metadata).thenReturn(metadataBuilder.build()) + mediaDataManager.addListener(listener) + mediaDataManager.onNotificationAdded(KEY, mediaNotification) + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + assertThat(listener.data!!.active).isTrue() + + // Swiping away makes the notification not active + mediaDataManager.onSwipeToDismiss() + assertThat(mediaDataManager.hasActiveMedia()).isFalse() + + // And when a notification is updated + mediaDataManager.onNotificationAdded(KEY, mediaNotification) + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + + // MediaData should still be inactive + assertThat(mediaDataManager.hasActiveMedia()).isFalse() + } + + @Test fun testHasAnyMedia_whenAddingMedia() { assertThat(mediaDataManager.hasAnyMedia()).isFalse() val data = mock(MediaData::class.java) diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java index 1c0d451e064a..438de99015a4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java @@ -14,6 +14,10 @@ package com.android.systemui.qs.tileimpl; + +import static androidx.lifecycle.Lifecycle.State.DESTROYED; +import static androidx.lifecycle.Lifecycle.State.RESUMED; + import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_CLICK; import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_LONG_PRESS; import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_SECONDARY_CLICK; @@ -23,6 +27,9 @@ import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_ import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_ACTION; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; @@ -267,6 +274,42 @@ public class QSTileImplTest extends SysuiTestCase { verify(mQsLogger).logTileChangeListening(SPEC, false); } + @Test + public void testListeningTrue_stateAtLeastResumed() { + mTile.setListening(new Object(), true); // Listen with some object + + TestableLooper.get(this).processAllMessages(); + + assertTrue(mTile.getLifecycle().getCurrentState().isAtLeast(RESUMED)); + } + + @Test + public void testTileDoesntStartResumed() { + assertFalse(mTile.getLifecycle().getCurrentState().isAtLeast(RESUMED)); + } + + @Test + public void testListeningFalse_stateAtMostCreated() { + Object o = new Object(); + mTile.setListening(o, true); + + mTile.setListening(o, false); + + TestableLooper.get(this).processAllMessages(); + assertFalse(mTile.getLifecycle().getCurrentState().isAtLeast(RESUMED)); + } + + @Test + public void testListeningFalse_stateNotDestroyed() { + Object o = new Object(); + mTile.setListening(o, true); + + mTile.setListening(o, false); + + TestableLooper.get(this).processAllMessages(); + assertNotEquals(DESTROYED, mTile.getLifecycle().getCurrentState()); + } + private void assertEvent(UiEventLogger.UiEventEnum eventType, UiEventLoggerFake.FakeUiEvent fakeEvent) { assertEquals(eventType.getId(), fakeEvent.eventId); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java index e5024595d97e..5a6823879942 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java @@ -29,13 +29,12 @@ import android.testing.TestableLooper; import androidx.test.filters.SmallTest; -import com.android.internal.logging.UiEventLogger; import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; -import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.qs.QSTileHost; import com.android.systemui.screenrecord.RecordingController; +import com.android.systemui.statusbar.phone.KeyguardDismissUtil; import org.junit.Before; import org.junit.Test; @@ -44,18 +43,16 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; @RunWith(AndroidTestingRunner.class) -@TestableLooper.RunWithLooper +@TestableLooper.RunWithLooper(setAsMainLooper = true) @SmallTest public class ScreenRecordTileTest extends SysuiTestCase { @Mock private RecordingController mController; @Mock - private ActivityStarter mActivityStarter; - @Mock private QSTileHost mHost; @Mock - private UiEventLogger mUiEventLogger; + private KeyguardDismissUtil mKeyguardDismissUtil; private TestableLooper mTestableLooper; private ScreenRecordTile mTile; @@ -67,11 +64,10 @@ public class ScreenRecordTileTest extends SysuiTestCase { mTestableLooper = TestableLooper.get(this); mDependency.injectTestDependency(Dependency.BG_LOOPER, mTestableLooper.getLooper()); mController = mDependency.injectMockDependency(RecordingController.class); - mActivityStarter = mDependency.injectMockDependency(ActivityStarter.class); when(mHost.getContext()).thenReturn(mContext); - mTile = new ScreenRecordTile(mHost, mController, mActivityStarter, mUiEventLogger); + mTile = new ScreenRecordTile(mHost, mController, mKeyguardDismissUtil); } // Test that the tile is inactive and labeled correctly when the controller is neither starting @@ -89,6 +85,7 @@ public class ScreenRecordTileTest extends SysuiTestCase { mContext.getString(R.string.quick_settings_screen_record_start))); mTile.handleClick(); + mTestableLooper.processAllMessages(); verify(mController, times(1)).getPromptIntent(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java index b877c7fa6859..11ef3e33f9d0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java @@ -22,12 +22,14 @@ import static junit.framework.Assert.assertTrue; import static org.mockito.Mockito.verify; import android.app.PendingIntent; +import android.content.Intent; import android.os.Looper; import android.testing.AndroidTestingRunner; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; +import com.android.systemui.broadcast.BroadcastDispatcher; import org.junit.Before; import org.junit.Test; @@ -45,14 +47,18 @@ import org.mockito.MockitoAnnotations; public class RecordingControllerTest extends SysuiTestCase { @Mock - RecordingController.RecordingStateChangeCallback mCallback; + private RecordingController.RecordingStateChangeCallback mCallback; + @Mock + private BroadcastDispatcher mBroadcastDispatcher; + + private RecordingController mController; - RecordingController mController; + private static final int USER_ID = 10; @Before public void setUp() { MockitoAnnotations.initMocks(this); - mController = new RecordingController(mContext); + mController = new RecordingController(mBroadcastDispatcher); mController.addCallback(mCallback); } @@ -121,4 +127,27 @@ public class RecordingControllerTest extends SysuiTestCase { assertFalse(mController.isRecording()); verify(mCallback).onRecordingEnd(); } + + // Test that switching users will stop an ongoing recording + @Test + public void testUserChange() { + if (Looper.myLooper() == null) { + Looper.prepare(); + } + + // If we are recording + PendingIntent startIntent = Mockito.mock(PendingIntent.class); + PendingIntent stopIntent = Mockito.mock(PendingIntent.class); + mController.startCountdown(0, 0, startIntent, stopIntent); + mController.updateState(true); + + // and user is changed + Intent intent = new Intent(Intent.ACTION_USER_SWITCHED) + .putExtra(Intent.EXTRA_USER_HANDLE, USER_ID); + mController.mUserChangeReceiver.onReceive(mContext, intent); + + // Ensure that the recording was stopped + verify(mCallback).onRecordingEnd(); + assertFalse(mController.isRecording()); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java index 283a47ca3622..e98b6b69ee76 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java @@ -32,6 +32,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.logging.UiEventLogger; import com.android.systemui.SysuiTestCase; +import com.android.systemui.settings.CurrentUserContextTracker; import org.junit.Before; import org.junit.Test; @@ -58,6 +59,8 @@ public class RecordingServiceTest extends SysuiTestCase { private Notification mNotification; @Mock private Executor mExecutor; + @Mock + private CurrentUserContextTracker mUserContextTracker; private RecordingService mRecordingService; @@ -65,7 +68,7 @@ public class RecordingServiceTest extends SysuiTestCase { public void setUp() throws Exception { MockitoAnnotations.initMocks(this); mRecordingService = Mockito.spy(new RecordingService(mController, mExecutor, mUiEventLogger, - mNotificationManager)); + mNotificationManager, mUserContextTracker)); // Return actual context info doReturn(mContext).when(mRecordingService).getApplicationContext(); @@ -80,6 +83,8 @@ public class RecordingServiceTest extends SysuiTestCase { doNothing().when(mRecordingService).startForeground(anyInt(), any()); doReturn(mScreenMediaRecorder).when(mRecordingService).getRecorder(); + + doReturn(mContext).when(mUserContextTracker).getCurrentUserContext(); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java index c2d218140803..b0b66b87d421 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java @@ -180,6 +180,8 @@ public class NotificationPanelViewTest extends SysuiTestCase { @Mock private ConversationNotificationManager mConversationNotificationManager; @Mock + private BiometricUnlockController mBiometricUnlockController; + @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; private FlingAnimationUtils.Builder mFlingAnimationUtilsBuilder; @@ -238,7 +240,7 @@ public class NotificationPanelViewTest extends SysuiTestCase { mMetricsLogger, mActivityManager, mZenModeController, mConfigurationController, mFlingAnimationUtilsBuilder, mStatusBarTouchableRegionManager, mConversationNotificationManager, mMediaHiearchyManager, - mStatusBarKeyguardViewManager); + mBiometricUnlockController, mStatusBarKeyguardViewManager); mNotificationPanelViewController.initDependencies(mStatusBar, mGroupManager, mNotificationShelf, mNotificationAreaController, mScrimController); mNotificationPanelViewController.setHeadsUpManager(mHeadsUpManager); diff --git a/packages/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/packages/Tethering/common/TetheringLib/src/android/net/TetheringManager.java index cc095a0bb4a7..4f053cb65c38 100644 --- a/packages/Tethering/common/TetheringLib/src/android/net/TetheringManager.java +++ b/packages/Tethering/common/TetheringLib/src/android/net/TetheringManager.java @@ -171,6 +171,14 @@ public class TetheringManager { */ public static final int TETHERING_ETHERNET = 5; + /** + * WIGIG tethering type. Use a separate type to prevent + * conflicts with TETHERING_WIFI + * This type is only used internally by the tethering module + * @hide + */ + public static final int TETHERING_WIGIG = 6; + /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(value = { diff --git a/packages/Tethering/res/values/config.xml b/packages/Tethering/res/values/config.xml index 9269c6f0fd52..9b9dcde910e7 100644 --- a/packages/Tethering/res/values/config.xml +++ b/packages/Tethering/res/values/config.xml @@ -43,6 +43,13 @@ </string-array> <!-- List of regexpressions describing the interface (if any) that represent tetherable + WiGig interfaces. If the device doesn't want to support tethering over WiGig this + should be empty. An example would be "wigig\\d" --> + <string-array translatable="false" name="config_tether_wigig_regexs"> + <item>"wigig\\d"</item> + </string-array> + + <!-- List of regexpressions describing the interface (if any) that represent tetherable Wifi P2P interfaces. If the device doesn't want to support tethering over Wifi P2p this should be empty. An example would be "p2p-p2p\\d-.*" --> <string-array translatable="false" name="config_tether_wifi_p2p_regexs"> diff --git a/packages/Tethering/res/values/overlayable.xml b/packages/Tethering/res/values/overlayable.xml index 4e2bb1e31b2a..6a33d55cb0de 100644 --- a/packages/Tethering/res/values/overlayable.xml +++ b/packages/Tethering/res/values/overlayable.xml @@ -20,6 +20,7 @@ <item type="array" name="config_tether_usb_regexs"/> <item type="array" name="config_tether_ncm_regexs" /> <item type="array" name="config_tether_wifi_regexs"/> + <item type="array" name="config_tether_wigig_regexs"/> <item type="array" name="config_tether_wifi_p2p_regexs"/> <item type="array" name="config_tether_bluetooth_regexs"/> <item type="array" name="config_tether_dhcp_range"/> diff --git a/packages/Tethering/src/android/net/ip/IpServer.java b/packages/Tethering/src/android/net/ip/IpServer.java index 35c156304c77..8af1797a9dd7 100644 --- a/packages/Tethering/src/android/net/ip/IpServer.java +++ b/packages/Tethering/src/android/net/ip/IpServer.java @@ -617,7 +617,8 @@ public class IpServer extends StateMachine { final Boolean setIfaceUp; if (mInterfaceType == TetheringManager.TETHERING_WIFI || mInterfaceType == TetheringManager.TETHERING_WIFI_P2P - || mInterfaceType == TetheringManager.TETHERING_ETHERNET) { + || mInterfaceType == TetheringManager.TETHERING_ETHERNET + || mInterfaceType == TetheringManager.TETHERING_WIGIG) { // The WiFi and Ethernet stack has ownership of the interface up/down state. // It is unclear whether the Bluetooth or USB stacks will manage their own // state. diff --git a/packages/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java b/packages/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java index fe92204c25c8..33b9d00e70dc 100644 --- a/packages/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java +++ b/packages/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java @@ -16,8 +16,11 @@ package com.android.networkstack.tethering; +import static android.net.netlink.StructNlMsgHdr.NLM_F_DUMP; +import static android.net.netlink.StructNlMsgHdr.NLM_F_REQUEST; import static android.net.util.TetheringUtils.uint16; +import android.annotation.NonNull; import android.hardware.tetheroffload.config.V1_0.IOffloadConfig; import android.hardware.tetheroffload.control.V1_0.IOffloadControl; import android.hardware.tetheroffload.control.V1_0.ITetheringOffloadCallback; @@ -25,6 +28,7 @@ import android.hardware.tetheroffload.control.V1_0.NatTimeoutUpdate; import android.hardware.tetheroffload.control.V1_0.NetworkProtocol; import android.hardware.tetheroffload.control.V1_0.OffloadCallbackEvent; import android.net.netlink.NetlinkSocket; +import android.net.netlink.StructNlMsgHdr; import android.net.util.SharedLog; import android.net.util.SocketUtils; import android.os.Handler; @@ -37,9 +41,11 @@ import android.system.OsConstants; import com.android.internal.annotations.VisibleForTesting; import java.io.FileDescriptor; +import java.io.InterruptedIOException; import java.io.IOException; import java.net.SocketAddress; import java.net.SocketException; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.NoSuchElementException; @@ -63,6 +69,11 @@ public class OffloadHardwareInterface { private static final int NF_NETLINK_CONNTRACK_NEW = 1; private static final int NF_NETLINK_CONNTRACK_UPDATE = 2; private static final int NF_NETLINK_CONNTRACK_DESTROY = 4; + // Reference libnetfilter_conntrack/linux_nfnetlink_conntrack.h + public static final short NFNL_SUBSYS_CTNETLINK = 1; + public static final short IPCTNL_MSG_CT_GET = 1; + + private final long NETLINK_MESSAGE_TIMEOUT_MS = 500; private final Handler mHandler; private final SharedLog mLog; @@ -226,6 +237,9 @@ public class OffloadHardwareInterface { NF_NETLINK_CONNTRACK_NEW | NF_NETLINK_CONNTRACK_DESTROY); if (h1 == null) return false; + sendNetlinkMessage(h1, (short) ((NFNL_SUBSYS_CTNETLINK << 8) | IPCTNL_MSG_CT_GET), + (short) (NLM_F_REQUEST | NLM_F_DUMP)); + final NativeHandle h2 = mDeps.createConntrackSocket( NF_NETLINK_CONNTRACK_UPDATE | NF_NETLINK_CONNTRACK_DESTROY); if (h2 == null) { @@ -252,6 +266,25 @@ public class OffloadHardwareInterface { return results.mSuccess; } + @VisibleForTesting + public void sendNetlinkMessage(@NonNull NativeHandle handle, short type, short flags) { + final int length = StructNlMsgHdr.STRUCT_SIZE; + final byte[] msg = new byte[length]; + final StructNlMsgHdr nlh = new StructNlMsgHdr(); + final ByteBuffer byteBuffer = ByteBuffer.wrap(msg); + nlh.nlmsg_len = length; + nlh.nlmsg_type = type; + nlh.nlmsg_flags = flags; + nlh.nlmsg_seq = 1; + nlh.pack(byteBuffer); + try { + NetlinkSocket.sendMessage(handle.getFileDescriptor(), msg, 0 /* offset */, length, + NETLINK_MESSAGE_TIMEOUT_MS); + } catch (ErrnoException | InterruptedIOException e) { + mLog.e("Unable to send netfilter message, error: " + e); + } + } + private void closeFdInNativeHandle(final NativeHandle h) { try { h.close(); diff --git a/packages/Tethering/src/com/android/networkstack/tethering/Tethering.java b/packages/Tethering/src/com/android/networkstack/tethering/Tethering.java index c72ac52740d7..3695ec65d5c0 100644 --- a/packages/Tethering/src/com/android/networkstack/tethering/Tethering.java +++ b/packages/Tethering/src/com/android/networkstack/tethering/Tethering.java @@ -40,6 +40,7 @@ import static android.net.TetheringManager.TETHERING_NCM; import static android.net.TetheringManager.TETHERING_USB; import static android.net.TetheringManager.TETHERING_WIFI; import static android.net.TetheringManager.TETHERING_WIFI_P2P; +import static android.net.TetheringManager.TETHERING_WIGIG; import static android.net.TetheringManager.TETHER_ERROR_INTERNAL_ERROR; import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR; import static android.net.TetheringManager.TETHER_ERROR_SERVICE_UNAVAIL; @@ -495,7 +496,8 @@ public class Tethering { if (up) { maybeTrackNewInterfaceLocked(iface); } else { - if (ifaceNameToType(iface) == TETHERING_BLUETOOTH) { + if (ifaceNameToType(iface) == TETHERING_BLUETOOTH + || ifaceNameToType(iface) == TETHERING_WIGIG) { stopTrackingInterfaceLocked(iface); } else { // Ignore usb0 down after enabling RNDIS. @@ -517,6 +519,8 @@ public class Tethering { if (cfg.isWifi(iface)) { return TETHERING_WIFI; + } else if (cfg.isWigig(iface)) { + return TETHERING_WIGIG; } else if (cfg.isWifiP2p(iface)) { return TETHERING_WIFI_P2P; } else if (cfg.isUsb(iface)) { diff --git a/packages/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java b/packages/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java index 18b2b7804fb0..e1771a561370 100644 --- a/packages/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java +++ b/packages/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java @@ -92,6 +92,7 @@ public class TetheringConfiguration { public final String[] tetherableUsbRegexs; public final String[] tetherableWifiRegexs; + public final String[] tetherableWigigRegexs; public final String[] tetherableWifiP2pRegexs; public final String[] tetherableBluetoothRegexs; public final String[] tetherableNcmRegexs; @@ -125,6 +126,7 @@ public class TetheringConfiguration { // us an interface name. Careful consideration needs to be given to // implications for Settings and for provisioning checks. tetherableWifiRegexs = getResourceStringArray(res, R.array.config_tether_wifi_regexs); + tetherableWigigRegexs = getResourceStringArray(res, R.array.config_tether_wigig_regexs); tetherableWifiP2pRegexs = getResourceStringArray( res, R.array.config_tether_wifi_p2p_regexs); tetherableBluetoothRegexs = getResourceStringArray( @@ -167,6 +169,11 @@ public class TetheringConfiguration { return matchesDownstreamRegexs(iface, tetherableWifiRegexs); } + /** Check whether input interface belong to wigig.*/ + public boolean isWigig(String iface) { + return matchesDownstreamRegexs(iface, tetherableWigigRegexs); + } + /** Check whether this interface is Wifi P2P interface. */ public boolean isWifiP2p(String iface) { return matchesDownstreamRegexs(iface, tetherableWifiP2pRegexs); diff --git a/packages/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java b/packages/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java index f8ff1cb29c23..c543fad62dba 100644 --- a/packages/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java +++ b/packages/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java @@ -17,13 +17,17 @@ package com.android.networkstack.tethering; import static android.net.util.TetheringUtils.uint16; +import static android.system.OsConstants.SOCK_STREAM; +import static android.system.OsConstants.AF_UNIX; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.hardware.tetheroffload.config.V1_0.IOffloadConfig; import android.hardware.tetheroffload.control.V1_0.IOffloadControl; @@ -31,11 +35,14 @@ import android.hardware.tetheroffload.control.V1_0.ITetheringOffloadCallback; import android.hardware.tetheroffload.control.V1_0.NatTimeoutUpdate; import android.hardware.tetheroffload.control.V1_0.NetworkProtocol; import android.hardware.tetheroffload.control.V1_0.OffloadCallbackEvent; +import android.net.netlink.StructNlMsgHdr; import android.net.util.SharedLog; import android.os.Handler; import android.os.NativeHandle; import android.os.test.TestLooper; +import android.system.ErrnoException; import android.system.OsConstants; +import android.system.Os; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -47,6 +54,9 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.io.FileDescriptor; +import java.io.OutputStream; +import java.nio.ByteBuffer; import java.util.ArrayList; @RunWith(AndroidJUnit4.class) @@ -64,6 +74,10 @@ public final class OffloadHardwareInterfaceTest { @Mock private IOffloadControl mIOffloadControl; @Mock private NativeHandle mNativeHandle; + // Random values to test Netlink message. + private static final short TEST_TYPE = 184; + private static final short TEST_FLAGS = 263; + class MyDependencies extends OffloadHardwareInterface.Dependencies { MyDependencies(SharedLog log) { super(log); @@ -203,6 +217,31 @@ public final class OffloadHardwareInterfaceTest { eq(uint16(udpParams.dst.port))); } + @Test + public void testNetlinkMessage() throws Exception { + FileDescriptor writeSocket = new FileDescriptor(); + FileDescriptor readSocket = new FileDescriptor(); + try { + Os.socketpair(AF_UNIX, SOCK_STREAM, 0, writeSocket, readSocket); + } catch (ErrnoException e) { + fail(); + return; + } + when(mNativeHandle.getFileDescriptor()).thenReturn(writeSocket); + + mOffloadHw.sendNetlinkMessage(mNativeHandle, TEST_TYPE, TEST_FLAGS); + + ByteBuffer buffer = ByteBuffer.allocate(StructNlMsgHdr.STRUCT_SIZE); + int read = Os.read(readSocket, buffer); + + buffer.flip(); + assertEquals(StructNlMsgHdr.STRUCT_SIZE, buffer.getInt()); + assertEquals(TEST_TYPE, buffer.getShort()); + assertEquals(TEST_FLAGS, buffer.getShort()); + assertEquals(1 /* seq */, buffer.getInt()); + assertEquals(0 /* pid */, buffer.getInt()); + } + private NatTimeoutUpdate buildNatTimeoutUpdate(final int proto) { final NatTimeoutUpdate params = new NatTimeoutUpdate(); params.proto = proto; diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 7f912a4fc1ce..d2b1bd1a6008 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -56,6 +56,8 @@ import android.content.pm.PackageManagerInternal; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.database.ContentObserver; +import android.graphics.Point; +import android.graphics.Rect; import android.graphics.Region; import android.hardware.display.DisplayManager; import android.hardware.fingerprint.IFingerprintService; @@ -190,6 +192,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub private final SimpleStringSplitter mStringColonSplitter = new SimpleStringSplitter(COMPONENT_NAME_SEPARATOR); + private final Rect mTempRect = new Rect(); + private final Rect mTempRect1 = new Rect(); + private final PackageManager mPackageManager; private final PowerManager mPowerManager; @@ -246,6 +251,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub //TODO: Remove this hack private boolean mInitialized; + private Point mTempPoint; private boolean mIsAccessibilityButtonShown; private AccessibilityUserState getCurrentUserStateLocked() { @@ -1068,6 +1074,18 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } /** + * Gets a point within the accessibility focused node where we can send down + * and up events to perform a click. + * + * @param outPoint The click point to populate. + * @return Whether accessibility a click point was found and set. + */ + // TODO: (multi-display) Make sure this works for multiple displays. + public boolean getAccessibilityFocusClickPointInScreen(Point outPoint) { + return getInteractionBridge().getAccessibilityFocusClickPointInScreenNotLocked(outPoint); + } + + /** * Perform an accessibility action on the view that currently has accessibility focus. * Has no effect if no item has accessibility focus, if the item with accessibility * focus does not expose the specified action, or if the action fails. @@ -1081,6 +1099,32 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub return getInteractionBridge().performActionOnAccessibilityFocusedItemNotLocked(action); } + /** + * Returns true if accessibility focus is confined to the active window. + */ + public boolean accessibilityFocusOnlyInActiveWindow() { + synchronized (mLock) { + return mA11yWindowManager.isTrackingWindowsLocked(); + } + } + + /** + * Gets the bounds of a window. + * + * @param outBounds The output to which to write the bounds. + */ + boolean getWindowBounds(int windowId, Rect outBounds) { + IBinder token; + synchronized (mLock) { + token = getWindowToken(windowId, mCurrentUserId); + } + mWindowManagerService.getWindowFrame(token, outBounds); + if (!outBounds.isEmpty()) { + return true; + } + return false; + } + public int getActiveWindowId() { return mA11yWindowManager.getActiveWindowId(mCurrentUserId); } @@ -1824,9 +1868,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub for (int i = 0; !observingWindows && (i < boundServiceCount); i++) { AccessibilityServiceConnection boundService = boundServices.get(i); if (boundService.canRetrieveInteractiveWindowsLocked()) { + userState.setAccessibilityFocusOnlyInActiveWindow(false); observingWindows = true; } } + userState.setAccessibilityFocusOnlyInActiveWindow(true); // Gets all valid displays and start tracking windows of each display if there is at least // one bound service that can retrieve window content. @@ -2930,6 +2976,19 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } /** + * Gets a point within the accessibility focused node where we can send down and up events + * to perform a click. + * + * @param outPoint The click point to populate. + * @return Whether accessibility a click point was found and set. + */ + // TODO: (multi-display) Make sure this works for multiple displays. + boolean getAccessibilityFocusClickPointInScreen(Point outPoint) { + return getInteractionBridge() + .getAccessibilityFocusClickPointInScreenNotLocked(outPoint); + } + + /** * Perform an accessibility action on the view that currently has accessibility focus. * Has no effect if no item has accessibility focus, if the item with accessibility * focus does not expose the specified action, or if the action fails. @@ -2947,6 +3006,43 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub return focus.performAction(action.getId()); } + public boolean getAccessibilityFocusClickPointInScreenNotLocked(Point outPoint) { + AccessibilityNodeInfo focus = getAccessibilityFocusNotLocked(); + if (focus == null) { + return false; + } + + synchronized (mLock) { + Rect boundsInScreen = mTempRect; + focus.getBoundsInScreen(boundsInScreen); + + // Apply magnification if needed. + MagnificationSpec spec = getCompatibleMagnificationSpecLocked(focus.getWindowId()); + if (spec != null && !spec.isNop()) { + boundsInScreen.offset((int) -spec.offsetX, (int) -spec.offsetY); + boundsInScreen.scale(1 / spec.scale); + } + + // Clip to the window bounds. + Rect windowBounds = mTempRect1; + getWindowBounds(focus.getWindowId(), windowBounds); + if (!boundsInScreen.intersect(windowBounds)) { + return false; + } + + // Clip to the screen bounds. + Point screenSize = mTempPoint; + mDefaultDisplay.getRealSize(screenSize); + if (!boundsInScreen.intersect(0, 0, screenSize.x, screenSize.y)) { + return false; + } + + outPoint.set(boundsInScreen.centerX(), boundsInScreen.centerY()); + } + + return true; + } + private AccessibilityNodeInfo getAccessibilityFocusNotLocked() { final int focusedWindowId; synchronized (mLock) { diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java index 43bb4b384bb2..0845d019c060 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java @@ -104,6 +104,7 @@ class AccessibilityUserState { private boolean mIsDisplayMagnificationEnabled; private boolean mIsFilterKeyEventsEnabled; private boolean mIsPerformGesturesEnabled; + private boolean mAccessibilityFocusOnlyInActiveWindow; private boolean mIsTextHighContrastEnabled; private boolean mIsTouchExplorationEnabled; private boolean mServiceHandlesDoubleTap; @@ -685,6 +686,13 @@ class AccessibilityUserState { mIsPerformGesturesEnabled = enabled; } + public boolean isAccessibilityFocusOnlyInActiveWindow() { + return mAccessibilityFocusOnlyInActiveWindow; + } + + public void setAccessibilityFocusOnlyInActiveWindow(boolean enabled) { + mAccessibilityFocusOnlyInActiveWindow = enabled; + } public ComponentName getServiceChangingSoftKeyboardModeLocked() { return mServiceChangingSoftKeyboardMode; } diff --git a/services/accessibility/java/com/android/server/accessibility/FullScreenMagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/FullScreenMagnificationGestureHandler.java index afe6238ca38f..b7f8e674f3ba 100644 --- a/services/accessibility/java/com/android/server/accessibility/FullScreenMagnificationGestureHandler.java +++ b/services/accessibility/java/com/android/server/accessibility/FullScreenMagnificationGestureHandler.java @@ -26,6 +26,7 @@ import static android.view.MotionEvent.ACTION_UP; import static com.android.internal.accessibility.util.AccessibilityStatsLogUtils.logMagnificationTripleTap; import static com.android.server.accessibility.gestures.GestureUtils.distance; +import static com.android.server.accessibility.gestures.GestureUtils.distanceClosestPointerToPoint; import static java.lang.Math.abs; import static java.util.Arrays.asList; @@ -37,6 +38,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.graphics.PointF; import android.os.Handler; import android.os.Looper; import android.os.Message; @@ -615,6 +617,7 @@ class FullScreenMagnificationGestureHandler extends MagnificationGestureHandler private static final int MESSAGE_ON_TRIPLE_TAP_AND_HOLD = 1; private static final int MESSAGE_TRANSITION_TO_DELEGATING_STATE = 2; + private static final int MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE = 3; final int mLongTapMinDelay; final int mSwipeMinDistance; @@ -626,6 +629,7 @@ class FullScreenMagnificationGestureHandler extends MagnificationGestureHandler private MotionEvent mPreLastDown; private MotionEvent mLastUp; private MotionEvent mPreLastUp; + private PointF mSecondPointerDownLocation = new PointF(Float.NaN, Float.NaN); private long mLastDetectingDownEventTime; @@ -656,6 +660,10 @@ class FullScreenMagnificationGestureHandler extends MagnificationGestureHandler transitionToDelegatingStateAndClear(); } break; + case MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE: { + transitToPanningScalingStateAndClear(); + } + break; default: { throw new IllegalArgumentException("Unknown message type: " + type); } @@ -702,14 +710,20 @@ class FullScreenMagnificationGestureHandler extends MagnificationGestureHandler } break; case ACTION_POINTER_DOWN: { - if (mMagnificationController.isMagnifying(mDisplayId)) { - transitionTo(mPanningScalingState); - clear(); + if (mMagnificationController.isMagnifying(mDisplayId) + && event.getPointerCount() == 2) { + storeSecondPointerDownLocation(event); + mHandler.sendEmptyMessageDelayed(MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE, + ViewConfiguration.getTapTimeout()); } else { transitionToDelegatingStateAndClear(); } } break; + case ACTION_POINTER_UP: { + transitionToDelegatingStateAndClear(); + } + break; case ACTION_MOVE: { if (isFingerDown() && distance(mLastDown, /* move */ event) > mSwipeMinDistance) { @@ -719,11 +733,19 @@ class FullScreenMagnificationGestureHandler extends MagnificationGestureHandler // For convenience, viewport dragging takes precedence // over insta-delegating on 3tap&swipe // (which is a rare combo to be used aside from magnification) - if (isMultiTapTriggered(2 /* taps */)) { + if (isMultiTapTriggered(2 /* taps */) && event.getPointerCount() == 1) { transitionToViewportDraggingStateAndClear(event); + } else if (isMagnifying() && event.getPointerCount() == 2) { + //Primary pointer is swiping, so transit to PanningScalingState + transitToPanningScalingStateAndClear(); } else { transitionToDelegatingStateAndClear(); } + } else if (isMagnifying() && secondPointerDownValid() + && distanceClosestPointerToPoint( + mSecondPointerDownLocation, /* move */ event) > mSwipeMinDistance) { + //Second pointer is swiping, so transit to PanningScalingState + transitToPanningScalingStateAndClear(); } } break; @@ -755,6 +777,21 @@ class FullScreenMagnificationGestureHandler extends MagnificationGestureHandler } } + private void storeSecondPointerDownLocation(MotionEvent event) { + final int index = event.getActionIndex(); + mSecondPointerDownLocation.set(event.getX(index), event.getY(index)); + } + + private boolean secondPointerDownValid() { + return !(Float.isNaN(mSecondPointerDownLocation.x) && Float.isNaN( + mSecondPointerDownLocation.y)); + } + + private void transitToPanningScalingStateAndClear() { + transitionTo(mPanningScalingState); + clear(); + } + public boolean isMultiTapTriggered(int numTaps) { // Shortcut acts as the 2 initial taps @@ -822,11 +859,13 @@ class FullScreenMagnificationGestureHandler extends MagnificationGestureHandler setShortcutTriggered(false); removePendingDelayedMessages(); clearDelayedMotionEvents(); + mSecondPointerDownLocation.set(Float.NaN, Float.NaN); } private void removePendingDelayedMessages() { mHandler.removeMessages(MESSAGE_ON_TRIPLE_TAP_AND_HOLD); mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE); + mHandler.removeMessages(MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE); } private void cacheDelayedMotionEvent(MotionEvent event, MotionEvent rawEvent, @@ -890,6 +929,7 @@ class FullScreenMagnificationGestureHandler extends MagnificationGestureHandler transitionTo(mDelegatingState); sendDelayedMotionEvents(); removePendingDelayedMessages(); + mSecondPointerDownLocation.set(Float.NaN, Float.NaN); } private void onTripleTap(MotionEvent up) { @@ -907,6 +947,10 @@ class FullScreenMagnificationGestureHandler extends MagnificationGestureHandler } } + private boolean isMagnifying() { + return mMagnificationController.isMagnifying(mDisplayId); + } + void transitionToViewportDraggingStateAndClear(MotionEvent down) { if (DEBUG_DETECTING) Slog.i(LOG_TAG, "onTripleTapAndHold()"); diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/EventDispatcher.java b/services/accessibility/java/com/android/server/accessibility/gestures/EventDispatcher.java index 667364c9c901..c8cee1079e8e 100644 --- a/services/accessibility/java/com/android/server/accessibility/gestures/EventDispatcher.java +++ b/services/accessibility/java/com/android/server/accessibility/gestures/EventDispatcher.java @@ -21,8 +21,11 @@ import static com.android.server.accessibility.gestures.TouchState.ALL_POINTER_I import static com.android.server.accessibility.gestures.TouchState.MAX_POINTER_COUNT; import android.content.Context; +import android.graphics.Point; import android.util.Slog; import android.view.MotionEvent; +import android.view.MotionEvent.PointerCoords; +import android.view.MotionEvent.PointerProperties; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; @@ -37,19 +40,27 @@ import com.android.server.policy.WindowManagerPolicy; */ class EventDispatcher { private static final String LOG_TAG = "EventDispatcher"; + private static final int CLICK_LOCATION_NONE = 0; + private static final int CLICK_LOCATION_ACCESSIBILITY_FOCUS = 1; + private static final int CLICK_LOCATION_LAST_TOUCH_EXPLORED = 2; private final AccessibilityManagerService mAms; private Context mContext; // The receiver of motion events. private EventStreamTransformation mReceiver; - // Keep track of which pointers sent to the system are down. - private int mInjectedPointersDown; - // The time of the last injected down. - private long mLastInjectedDownEventTime; + // The long pressing pointer id if coordinate remapping is needed for double tap and hold + private int mLongPressingPointerId = -1; + + // The long pressing pointer X if coordinate remapping is needed for double tap and hold. + private int mLongPressingPointerDeltaX; + + // The long pressing pointer Y if coordinate remapping is needed for double tap and hold. + private int mLongPressingPointerDeltaY; + + // Temporary point to avoid instantiation. + private final Point mTempPoint = new Point(); - // The last injected hover event. - private MotionEvent mLastInjectedHoverEvent; private TouchState mState; EventDispatcher( @@ -98,8 +109,18 @@ class EventDispatcher { if (action == MotionEvent.ACTION_DOWN) { event.setDownTime(event.getEventTime()); } else { - event.setDownTime(getLastInjectedDownEventTime()); + event.setDownTime(mState.getLastInjectedDownEventTime()); + } + // If the user is long pressing but the long pressing pointer + // was not exactly over the accessibility focused item we need + // to remap the location of that pointer so the user does not + // have to explicitly touch explore something to be able to + // long press it, or even worse to avoid the user long pressing + // on the wrong item since click and long press behave differently. + if (mLongPressingPointerId >= 0) { + event = offsetEvent(event, -mLongPressingPointerDeltaX, -mLongPressingPointerDeltaY); } + if (DEBUG) { Slog.d( LOG_TAG, @@ -116,7 +137,7 @@ class EventDispatcher { } else { Slog.e(LOG_TAG, "Error sending event: no receiver specified."); } - updateState(event); + mState.onInjectedMotionEvent(event); if (event != prototype) { event.recycle(); @@ -145,87 +166,15 @@ class EventDispatcher { mState.onInjectedAccessibilityEvent(type); } - /** - * Processes an injected {@link MotionEvent} event. - * - * @param event The event to process. - */ - void updateState(MotionEvent event) { - final int action = event.getActionMasked(); - final int pointerId = event.getPointerId(event.getActionIndex()); - final int pointerFlag = (1 << pointerId); - switch (action) { - case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_POINTER_DOWN: - mInjectedPointersDown |= pointerFlag; - mLastInjectedDownEventTime = event.getDownTime(); - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_POINTER_UP: - mInjectedPointersDown &= ~pointerFlag; - if (mInjectedPointersDown == 0) { - mLastInjectedDownEventTime = 0; - } - break; - case MotionEvent.ACTION_HOVER_ENTER: - case MotionEvent.ACTION_HOVER_MOVE: - case MotionEvent.ACTION_HOVER_EXIT: - if (mLastInjectedHoverEvent != null) { - mLastInjectedHoverEvent.recycle(); - } - mLastInjectedHoverEvent = MotionEvent.obtain(event); - break; - } - if (DEBUG) { - Slog.i(LOG_TAG, "Injected pointer:\n" + toString()); - } - } - - /** Clears the internals state. */ - public void clear() { - mInjectedPointersDown = 0; - } - - /** @return The time of the last injected down event. */ - public long getLastInjectedDownEventTime() { - return mLastInjectedDownEventTime; - } - - /** @return The number of down pointers injected to the view hierarchy. */ - public int getInjectedPointerDownCount() { - return Integer.bitCount(mInjectedPointersDown); - } - - /** @return The bits of the injected pointers that are down. */ - public int getInjectedPointersDown() { - return mInjectedPointersDown; - } - - /** - * Whether an injected pointer is down. - * - * @param pointerId The unique pointer id. - * @return True if the pointer is down. - */ - public boolean isInjectedPointerDown(int pointerId) { - final int pointerFlag = (1 << pointerId); - return (mInjectedPointersDown & pointerFlag) != 0; - } - - /** @return The the last injected hover event. */ - public MotionEvent getLastInjectedHoverEvent() { - return mLastInjectedHoverEvent; - } - @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("========================="); builder.append("\nDown pointers #"); - builder.append(Integer.bitCount(mInjectedPointersDown)); + builder.append(Integer.bitCount(mState.getInjectedPointersDown())); builder.append(" [ "); for (int i = 0; i < MAX_POINTER_COUNT; i++) { - if ((mInjectedPointersDown & i) != 0) { + if (mState.isInjectedPointerDown(i)) { builder.append(i); builder.append(" "); } @@ -236,6 +185,48 @@ class EventDispatcher { } /** + * /** Offsets all pointers in the given event by adding the specified X and Y offsets. + * + * @param event The event to offset. + * @param offsetX The X offset. + * @param offsetY The Y offset. + * @return An event with the offset pointers or the original event if both offsets are zero. + */ + private MotionEvent offsetEvent(MotionEvent event, int offsetX, int offsetY) { + if (offsetX == 0 && offsetY == 0) { + return event; + } + final int remappedIndex = event.findPointerIndex(mLongPressingPointerId); + final int pointerCount = event.getPointerCount(); + PointerProperties[] props = PointerProperties.createArray(pointerCount); + PointerCoords[] coords = PointerCoords.createArray(pointerCount); + for (int i = 0; i < pointerCount; i++) { + event.getPointerProperties(i, props[i]); + event.getPointerCoords(i, coords[i]); + if (i == remappedIndex) { + coords[i].x += offsetX; + coords[i].y += offsetY; + } + } + return MotionEvent.obtain( + event.getDownTime(), + event.getEventTime(), + event.getAction(), + event.getPointerCount(), + props, + coords, + event.getMetaState(), + event.getButtonState(), + 1.0f, + 1.0f, + event.getDeviceId(), + event.getEdgeFlags(), + event.getSource(), + event.getDisplayId(), + event.getFlags()); + } + + /** * Computes the action for an injected event based on a masked action and a pointer index. * * @param actionMasked The masked action. @@ -247,7 +238,7 @@ class EventDispatcher { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: // Compute the action based on how many down pointers are injected. - if (getInjectedPointerDownCount() == 0) { + if (mState.getInjectedPointerDownCount() == 0) { return MotionEvent.ACTION_DOWN; } else { return (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT) @@ -255,7 +246,7 @@ class EventDispatcher { } case MotionEvent.ACTION_POINTER_UP: // Compute the action based on how many down pointers are injected. - if (getInjectedPointerDownCount() == 1) { + if (mState.getInjectedPointerDownCount() == 1) { return MotionEvent.ACTION_UP; } else { return (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT) @@ -280,7 +271,7 @@ class EventDispatcher { for (int i = 0; i < pointerCount; i++) { final int pointerId = prototype.getPointerId(i); // Do not send event for already delivered pointers. - if (!isInjectedPointerDown(pointerId)) { + if (!mState.isInjectedPointerDown(pointerId)) { pointerIdBits |= (1 << pointerId); final int action = computeInjectionAction(MotionEvent.ACTION_DOWN, i); sendMotionEvent( @@ -306,7 +297,7 @@ class EventDispatcher { for (int i = 0; i < pointerCount; i++) { final int pointerId = prototype.getPointerId(i); // Skip non injected down pointers. - if (!isInjectedPointerDown(pointerId)) { + if (!mState.isInjectedPointerDown(pointerId)) { continue; } final int action = computeInjectionAction(MotionEvent.ACTION_POINTER_UP, i); @@ -315,4 +306,97 @@ class EventDispatcher { pointerIdBits &= ~(1 << pointerId); } } + + public boolean longPressWithTouchEvents(MotionEvent event, int policyFlags) { + final int pointerIndex = event.getActionIndex(); + final int pointerId = event.getPointerId(pointerIndex); + Point clickLocation = mTempPoint; + final int result = computeClickLocation(clickLocation); + if (result == CLICK_LOCATION_NONE) { + return false; + } + mLongPressingPointerId = pointerId; + mLongPressingPointerDeltaX = (int) event.getX(pointerIndex) - clickLocation.x; + mLongPressingPointerDeltaY = (int) event.getY(pointerIndex) - clickLocation.y; + sendDownForAllNotInjectedPointers(event, policyFlags); + return true; + } + + public void clickWithTouchEvents(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + final int pointerIndex = event.getActionIndex(); + final int pointerId = event.getPointerId(pointerIndex); + Point clickLocation = mTempPoint; + final int result = computeClickLocation(clickLocation); + if (result == CLICK_LOCATION_NONE) { + Slog.e(LOG_TAG, "Unable to compute click location."); + // We can't send a click to no location, but the gesture was still + // consumed. + return; + } + // Do the click. + PointerProperties[] properties = new PointerProperties[1]; + properties[0] = new PointerProperties(); + event.getPointerProperties(pointerIndex, properties[0]); + PointerCoords[] coords = new PointerCoords[1]; + coords[0] = new PointerCoords(); + coords[0].x = clickLocation.x; + coords[0].y = clickLocation.y; + MotionEvent clickEvent = + MotionEvent.obtain( + event.getDownTime(), + event.getEventTime(), + MotionEvent.ACTION_DOWN, + 1, + properties, + coords, + 0, + 0, + 1.0f, + 1.0f, + event.getDeviceId(), + 0, + event.getSource(), + event.getDisplayId(), + event.getFlags()); + final boolean targetAccessibilityFocus = (result == CLICK_LOCATION_ACCESSIBILITY_FOCUS); + sendActionDownAndUp(clickEvent, rawEvent, policyFlags, targetAccessibilityFocus); + clickEvent.recycle(); + } + + private int computeClickLocation(Point outLocation) { + if (mState.getLastInjectedHoverEventForClick() != null) { + final int lastExplorePointerIndex = + mState.getLastInjectedHoverEventForClick().getActionIndex(); + outLocation.x = + (int) mState.getLastInjectedHoverEventForClick().getX(lastExplorePointerIndex); + outLocation.y = + (int) mState.getLastInjectedHoverEventForClick().getY(lastExplorePointerIndex); + if (!mAms.accessibilityFocusOnlyInActiveWindow() + || mState.getLastTouchedWindowId() == mAms.getActiveWindowId()) { + if (mAms.getAccessibilityFocusClickPointInScreen(outLocation)) { + return CLICK_LOCATION_ACCESSIBILITY_FOCUS; + } else { + return CLICK_LOCATION_LAST_TOUCH_EXPLORED; + } + } + } + if (mAms.getAccessibilityFocusClickPointInScreen(outLocation)) { + return CLICK_LOCATION_ACCESSIBILITY_FOCUS; + } + return CLICK_LOCATION_NONE; + } + + private void sendActionDownAndUp( + MotionEvent prototype, + MotionEvent rawEvent, + int policyFlags, + boolean targetAccessibilityFocus) { + // Tap with the pointer that last explored. + final int pointerId = prototype.getPointerId(prototype.getActionIndex()); + final int pointerIdBits = (1 << pointerId); + prototype.setTargetAccessibilityFocus(targetAccessibilityFocus); + sendMotionEvent(prototype, MotionEvent.ACTION_DOWN, rawEvent, pointerIdBits, policyFlags); + prototype.setTargetAccessibilityFocus(targetAccessibilityFocus); + sendMotionEvent(prototype, MotionEvent.ACTION_UP, rawEvent, pointerIdBits, policyFlags); + } } diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/GestureManifold.java b/services/accessibility/java/com/android/server/accessibility/gestures/GestureManifold.java index 6d0f069e51ac..e9c70c60a322 100644 --- a/services/accessibility/java/com/android/server/accessibility/gestures/GestureManifold.java +++ b/services/accessibility/java/com/android/server/accessibility/gestures/GestureManifold.java @@ -104,6 +104,7 @@ class GestureManifold implements GestureMatcher.StateChangeListener { mHandler = new Handler(context.getMainLooper()); mListener = listener; mState = state; + mMultiFingerGesturesEnabled = false; // Set up gestures. // Start with double tap. mGestures.add(new MultiTap(context, 2, GESTURE_DOUBLE_TAP, this)); @@ -247,7 +248,7 @@ class GestureManifold implements GestureMatcher.StateChangeListener { * and hold is dispatched via onGestureCompleted. Otherwise, this method is called when the * user has performed a double tap and then held down the second tap. */ - void onDoubleTapAndHold(); + void onDoubleTapAndHold(MotionEvent event, MotionEvent rawEvent, int policyFlags); /** * When FLAG_SERVICE_HANDLES_DOUBLE_TAP is enabled, this method is not called; double-tap is @@ -256,7 +257,7 @@ class GestureManifold implements GestureMatcher.StateChangeListener { * * @return true if the event is consumed, else false */ - boolean onDoubleTap(); + boolean onDoubleTap(MotionEvent event, MotionEvent rawEvent, int policyFlags); /** * Called when the system has decided the event stream is a potential gesture. @@ -322,7 +323,7 @@ class GestureManifold implements GestureMatcher.StateChangeListener { new AccessibilityGestureEvent(gestureId, event.getDisplayId()); mListener.onGestureCompleted(gestureEvent); } else { - mListener.onDoubleTap(); + mListener.onDoubleTap(event, rawEvent, policyFlags); } clear(); break; @@ -332,7 +333,7 @@ class GestureManifold implements GestureMatcher.StateChangeListener { new AccessibilityGestureEvent(gestureId, event.getDisplayId()); mListener.onGestureCompleted(gestureEvent); } else { - mListener.onDoubleTapAndHold(); + mListener.onDoubleTapAndHold(event, rawEvent, policyFlags); } clear(); break; diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/GestureUtils.java b/services/accessibility/java/com/android/server/accessibility/gestures/GestureUtils.java index ac6748089314..ec3041848356 100644 --- a/services/accessibility/java/com/android/server/accessibility/gestures/GestureUtils.java +++ b/services/accessibility/java/com/android/server/accessibility/gestures/GestureUtils.java @@ -1,5 +1,6 @@ package com.android.server.accessibility.gestures; +import android.graphics.PointF; import android.util.MathUtils; import android.view.MotionEvent; @@ -38,6 +39,27 @@ public final class GestureUtils { return MathUtils.dist(first.getX(), first.getY(), second.getX(), second.getY()); } + /** + * Returns the minimum distance between {@code pointerDown} and each pointer of + * {@link MotionEvent}. + * + * @param pointerDown The action pointer location of the {@link MotionEvent} with + * {@link MotionEvent#ACTION_DOWN} or {@link MotionEvent#ACTION_POINTER_DOWN} + * @param moveEvent The {@link MotionEvent} with {@link MotionEvent#ACTION_MOVE} + * @return the movement of the pointer. + */ + public static double distanceClosestPointerToPoint(PointF pointerDown, MotionEvent moveEvent) { + float movement = Float.MAX_VALUE; + for (int i = 0; i < moveEvent.getPointerCount(); i++) { + final float moveDelta = MathUtils.dist(pointerDown.x, pointerDown.y, moveEvent.getX(i), + moveEvent.getY(i)); + if (movement > moveDelta) { + movement = moveDelta; + } + } + return movement; + } + public static boolean isTimedOut(MotionEvent firstUp, MotionEvent secondUp, int timeout) { final long deltaTime = secondUp.getEventTime() - firstUp.getEventTime(); return (deltaTime >= timeout); diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java index 4fee672a8803..373d47ed366b 100644 --- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java +++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java @@ -22,7 +22,6 @@ import static com.android.server.accessibility.gestures.TouchState.ALL_POINTER_I import android.accessibilityservice.AccessibilityGestureEvent; import android.content.Context; -import android.graphics.Point; import android.graphics.Region; import android.os.Handler; import android.util.Slog; @@ -86,6 +85,7 @@ public class TouchExplorer extends BaseEventStreamTransformation // The ID of the pointer used for dragging. private int mDraggingPointerId; + // Handler for performing asynchronous operations. private final Handler mHandler; @@ -115,8 +115,6 @@ public class TouchExplorer extends BaseEventStreamTransformation // Handle to the accessibility manager service. private final AccessibilityManagerService mAms; - // Temporary point to avoid instantiation. - private final Point mTempPoint = new Point(); // Context in which this explorer operates. private final Context mContext; @@ -277,6 +275,7 @@ public class TouchExplorer extends BaseEventStreamTransformation if (eventType == AccessibilityEvent.TYPE_VIEW_HOVER_EXIT) { sendsPendingA11yEventsIfNeed(); } + mState.onReceivedAccessibilityEvent(event); super.onAccessibilityEvent(event); } @@ -309,16 +308,20 @@ public class TouchExplorer extends BaseEventStreamTransformation } @Override - public void onDoubleTapAndHold() { + public void onDoubleTapAndHold(MotionEvent event, MotionEvent rawEvent, int policyFlags) { // Try to use the standard accessibility API to long click if (!mAms.performActionOnAccessibilityFocusedItem( AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK)) { Slog.e(LOG_TAG, "ACTION_LONG_CLICK failed."); + if (mDispatcher.longPressWithTouchEvents(event, policyFlags)) { + sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags); + mState.startDelegating(); + } } } @Override - public boolean onDoubleTap() { + public boolean onDoubleTap(MotionEvent event, MotionEvent rawEvent, int policyFlags) { mAms.onTouchInteractionEnd(); // Remove pending event deliveries. mSendHoverEnterAndMoveDelayed.cancel(); @@ -334,7 +337,10 @@ public class TouchExplorer extends BaseEventStreamTransformation // Try to use the standard accessibility API to click if (!mAms.performActionOnAccessibilityFocusedItem( AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK)) { - Slog.e(LOG_TAG, "ACTION_CLICK failed."); + Slog.e(LOG_TAG, "ACTION_CLICK failed. Dispatching motion events to simulate click."); + + mDispatcher.clickWithTouchEvents(event, rawEvent, policyFlags); + return true; } return true; } @@ -840,7 +846,7 @@ public class TouchExplorer extends BaseEventStreamTransformation * @param policyFlags The policy flags associated with the event. */ private void sendHoverExitAndTouchExplorationGestureEndIfNeeded(int policyFlags) { - MotionEvent event = mDispatcher.getLastInjectedHoverEvent(); + MotionEvent event = mState.getLastInjectedHoverEvent(); if (event != null && event.getActionMasked() != MotionEvent.ACTION_HOVER_EXIT) { final int pointerIdBits = event.getPointerIdBits(); if (!mSendTouchExplorationEndDelayed.isPending()) { @@ -862,7 +868,7 @@ public class TouchExplorer extends BaseEventStreamTransformation * @param policyFlags The policy flags associated with the event. */ private void sendTouchExplorationGestureStartAndHoverEnterIfNeeded(int policyFlags) { - MotionEvent event = mDispatcher.getLastInjectedHoverEvent(); + MotionEvent event = mState.getLastInjectedHoverEvent(); if (event != null && event.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT) { final int pointerIdBits = event.getPointerIdBits(); mDispatcher.sendMotionEvent( @@ -1188,7 +1194,6 @@ public class TouchExplorer extends BaseEventStreamTransformation + ", mDetermineUserIntentTimeout: " + mDetermineUserIntentTimeout + ", mDoubleTapSlop: " + mDoubleTapSlop + ", mDraggingPointerId: " + mDraggingPointerId - + ", mTempPoint: " + mTempPoint + " }"; } } diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java index d23dbbefd325..7a39bc29e8e5 100644 --- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java +++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java @@ -75,6 +75,16 @@ public class TouchState { private MotionEvent mLastReceivedEvent; // The accompanying raw event without any transformations. private MotionEvent mLastReceivedRawEvent; + // The id of the last touch explored window. + private int mLastTouchedWindowId; + // The last injected hover event. + private MotionEvent mLastInjectedHoverEvent; + // The last injected hover event used for performing clicks. + private MotionEvent mLastInjectedHoverEventForClick; + // The time of the last injected down. + private long mLastInjectedDownEventTime; + // Keep track of which pointers sent to the system are down. + private int mInjectedPointersDown; public TouchState() { mReceivedPointerTracker = new ReceivedPointerTracker(); @@ -88,7 +98,9 @@ public class TouchState { mLastReceivedEvent.recycle(); mLastReceivedEvent = null; } + mLastTouchedWindowId = -1; mReceivedPointerTracker.clear(); + mInjectedPointersDown = 0; } /** @@ -107,6 +119,71 @@ public class TouchState { mReceivedPointerTracker.onMotionEvent(rawEvent); } + /** + * Processes an injected {@link MotionEvent} event. + * + * @param event The event to process. + */ + void onInjectedMotionEvent(MotionEvent event) { + final int action = event.getActionMasked(); + final int pointerId = event.getPointerId(event.getActionIndex()); + final int pointerFlag = (1 << pointerId); + switch (action) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: + mInjectedPointersDown |= pointerFlag; + mLastInjectedDownEventTime = event.getDownTime(); + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + mInjectedPointersDown &= ~pointerFlag; + if (mInjectedPointersDown == 0) { + mLastInjectedDownEventTime = 0; + } + break; + case MotionEvent.ACTION_HOVER_ENTER: + case MotionEvent.ACTION_HOVER_MOVE: + if (mLastInjectedHoverEvent != null) { + mLastInjectedHoverEvent.recycle(); + } + mLastInjectedHoverEvent = MotionEvent.obtain(event); + break; + case MotionEvent.ACTION_HOVER_EXIT: + if (mLastInjectedHoverEvent != null) { + mLastInjectedHoverEvent.recycle(); + } + mLastInjectedHoverEvent = MotionEvent.obtain(event); + if (mLastInjectedHoverEventForClick != null) { + mLastInjectedHoverEventForClick.recycle(); + } + mLastInjectedHoverEventForClick = MotionEvent.obtain(event); + break; + } + if (DEBUG) { + Slog.i(LOG_TAG, "Injected pointer:\n" + toString()); + } + } + + /** Updates state in response to an accessibility event received from the outside. */ + public void onReceivedAccessibilityEvent(AccessibilityEvent event) { + // If a new window opens or the accessibility focus moves we no longer + // want to click/long press on the last touch explored location. + switch (event.getEventType()) { + case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: + case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED: + if (mLastInjectedHoverEventForClick != null) { + mLastInjectedHoverEventForClick.recycle(); + mLastInjectedHoverEventForClick = null; + } + mLastTouchedWindowId = -1; + break; + case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER: + case AccessibilityEvent.TYPE_VIEW_HOVER_EXIT: + mLastTouchedWindowId = event.getWindowId(); + break; + } + } + public void onInjectedAccessibilityEvent(int type) { // The below state transitions go here because the related events are often sent on a // delay. @@ -236,6 +313,46 @@ public class TouchState { return mLastReceivedEvent; } + /** @return The the last injected hover event. */ + public MotionEvent getLastInjectedHoverEvent() { + return mLastInjectedHoverEvent; + } + + /** @return The time of the last injected down event. */ + public long getLastInjectedDownEventTime() { + return mLastInjectedDownEventTime; + } + + public int getLastTouchedWindowId() { + return mLastTouchedWindowId; + } + + /** @return The number of down pointers injected to the view hierarchy. */ + public int getInjectedPointerDownCount() { + return Integer.bitCount(mInjectedPointersDown); + } + + /** @return The bits of the injected pointers that are down. */ + public int getInjectedPointersDown() { + return mInjectedPointersDown; + } + + /** + * Whether an injected pointer is down. + * + * @param pointerId The unique pointer id. + * @return True if the pointer is down. + */ + public boolean isInjectedPointerDown(int pointerId) { + final int pointerFlag = (1 << pointerId); + return (mInjectedPointersDown & pointerFlag) != 0; + } + + /** @return The the last injected hover event used for a click. */ + public MotionEvent getLastInjectedHoverEventForClick() { + return mLastInjectedHoverEventForClick; + } + /** This class tracks where and when a pointer went down. It does not track its movement. */ class ReceivedPointerTracker { private static final String LOG_TAG_RECEIVED_POINTER_TRACKER = "ReceivedPointerTracker"; diff --git a/services/autofill/java/com/android/server/autofill/RemoteAugmentedAutofillService.java b/services/autofill/java/com/android/server/autofill/RemoteAugmentedAutofillService.java index 11f901538868..533bbe68e274 100644 --- a/services/autofill/java/com/android/server/autofill/RemoteAugmentedAutofillService.java +++ b/services/autofill/java/com/android/server/autofill/RemoteAugmentedAutofillService.java @@ -149,7 +149,7 @@ final class RemoteAugmentedAutofillService @Nullable InlineSuggestionsRequest inlineSuggestionsRequest, @Nullable Function<InlineFillUi, Boolean> inlineSuggestionsCallback, @NonNull Runnable onErrorCallback, - @Nullable RemoteInlineSuggestionRenderService remoteRenderService) { + @Nullable RemoteInlineSuggestionRenderService remoteRenderService, int userId) { long requestTime = SystemClock.elapsedRealtime(); AtomicReference<ICancellationSignal> cancellationRef = new AtomicReference<>(); @@ -173,7 +173,7 @@ final class RemoteAugmentedAutofillService inlineSuggestionsRequest, inlineSuggestionsData, clientState, focusedId, focusedValue, inlineSuggestionsCallback, - client, onErrorCallback, remoteRenderService); + client, onErrorCallback, remoteRenderService, userId); if (!showingFillWindow) { requestAutofill.complete(null); } @@ -243,7 +243,8 @@ final class RemoteAugmentedAutofillService @NonNull AutofillId focusedId, @Nullable AutofillValue focusedValue, @Nullable Function<InlineFillUi, Boolean> inlineSuggestionsCallback, @NonNull IAutoFillManagerClient client, @NonNull Runnable onErrorCallback, - @Nullable RemoteInlineSuggestionRenderService remoteRenderService) { + @Nullable RemoteInlineSuggestionRenderService remoteRenderService, + int userId) { if (inlineSuggestionsData == null || inlineSuggestionsData.isEmpty() || inlineSuggestionsCallback == null || request == null || remoteRenderService == null) { @@ -312,7 +313,7 @@ final class RemoteAugmentedAutofillService Slog.w(TAG, "RemoteException starting intent sender"); } } - }, onErrorCallback, remoteRenderService); + }, onErrorCallback, remoteRenderService, userId, sessionId); if (inlineSuggestionsCallback.apply(inlineFillUi)) { mCallbacks.logAugmentedAutofillShown(sessionId, clientState); diff --git a/services/autofill/java/com/android/server/autofill/RemoteInlineSuggestionRenderService.java b/services/autofill/java/com/android/server/autofill/RemoteInlineSuggestionRenderService.java index 617c111c6c38..80b8583759e7 100644 --- a/services/autofill/java/com/android/server/autofill/RemoteInlineSuggestionRenderService.java +++ b/services/autofill/java/com/android/server/autofill/RemoteInlineSuggestionRenderService.java @@ -88,9 +88,9 @@ public final class RemoteInlineSuggestionRenderService extends */ public void renderSuggestion(@NonNull IInlineSuggestionUiCallback callback, @NonNull InlinePresentation presentation, int width, int height, - @Nullable IBinder hostInputToken, int displayId) { + @Nullable IBinder hostInputToken, int displayId, int userId, int sessionId) { scheduleAsyncRequest((s) -> s.renderSuggestion(callback, presentation, width, height, - hostInputToken, displayId)); + hostInputToken, displayId, userId, sessionId)); } /** @@ -100,6 +100,13 @@ public final class RemoteInlineSuggestionRenderService extends scheduleAsyncRequest((s) -> s.getInlineSuggestionsRendererInfo(callback)); } + /** + * Destroys the remote inline suggestion views associated with the given user id and session id. + */ + public void destroySuggestionViews(int userId, int sessionId) { + scheduleAsyncRequest((s) -> s.destroySuggestionViews(userId, sessionId)); + } + @Nullable private static ServiceInfo getServiceInfo(Context context, int userId) { final String packageName = diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 2b9ce2f07c70..9b3d075e3f2c 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -155,6 +155,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState */ public final int id; + /** userId the session belongs to */ + public final int userId; + /** uid the session is for */ public final int uid; @@ -823,6 +826,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } id = sessionId; mFlags = flags; + this.userId = userId; this.taskId = taskId; this.uid = uid; mStartTime = SystemClock.elapsedRealtime(); @@ -2986,7 +2990,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState mInlineSessionController.setInlineFillUiLocked( InlineFillUi.emptyUi(focusedId)); } - }, remoteRenderService); + }, remoteRenderService, userId, id); return mInlineSessionController.setInlineFillUiLocked(inlineFillUi); } @@ -3296,7 +3300,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState mInlineSessionController.setInlineFillUiLocked( InlineFillUi.emptyUi(mCurrentViewId)); } - }, mService.getRemoteInlineSuggestionRenderServiceLocked()); + }, mService.getRemoteInlineSuggestionRenderServiceLocked(), userId); } }; @@ -3796,6 +3800,12 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState if (mCurrentViewId != null) { mInlineSessionController.destroyLocked(mCurrentViewId); } + final RemoteInlineSuggestionRenderService remoteRenderService = + mService.getRemoteInlineSuggestionRenderServiceLocked(); + if (remoteRenderService != null) { + remoteRenderService.destroySuggestionViews(userId, id); + } + mDestroyed = true; // Log metrics diff --git a/services/autofill/java/com/android/server/autofill/ui/InlineFillUi.java b/services/autofill/java/com/android/server/autofill/ui/InlineFillUi.java index 627c0733b078..25e9d5c90764 100644 --- a/services/autofill/java/com/android/server/autofill/ui/InlineFillUi.java +++ b/services/autofill/java/com/android/server/autofill/ui/InlineFillUi.java @@ -105,19 +105,20 @@ public final class InlineFillUi { @NonNull AutofillId focusedViewId, @Nullable String filterText, @NonNull AutoFillUI.AutoFillUiCallback uiCallback, @NonNull Runnable onErrorCallback, - @Nullable RemoteInlineSuggestionRenderService remoteRenderService) { + @Nullable RemoteInlineSuggestionRenderService remoteRenderService, + int userId, int sessionId) { if (InlineSuggestionFactory.responseNeedAuthentication(response)) { InlineSuggestion inlineAuthentication = InlineSuggestionFactory.createInlineAuthentication(request, response, - focusedViewId, uiCallback, onErrorCallback, remoteRenderService); + uiCallback, onErrorCallback, remoteRenderService, userId, sessionId); return new InlineFillUi(focusedViewId, inlineAuthentication, filterText); } else if (response.getDatasets() != null) { SparseArray<Pair<Dataset, InlineSuggestion>> inlineSuggestions = InlineSuggestionFactory.createAutofillInlineSuggestions(request, response.getRequestId(), response.getDatasets(), focusedViewId, uiCallback, onErrorCallback, - remoteRenderService); + remoteRenderService, userId, sessionId); return new InlineFillUi(focusedViewId, inlineSuggestions, filterText); } return new InlineFillUi(focusedViewId, new SparseArray<>(), filterText); @@ -132,11 +133,12 @@ public final class InlineFillUi { @NonNull AutofillId focusedViewId, @Nullable String filterText, @NonNull InlineSuggestionUiCallback uiCallback, @NonNull Runnable onErrorCallback, - @Nullable RemoteInlineSuggestionRenderService remoteRenderService) { + @Nullable RemoteInlineSuggestionRenderService remoteRenderService, + int userId, int sessionId) { SparseArray<Pair<Dataset, InlineSuggestion>> inlineSuggestions = InlineSuggestionFactory.createAugmentedAutofillInlineSuggestions(request, datasets, focusedViewId, - uiCallback, onErrorCallback, remoteRenderService); + uiCallback, onErrorCallback, remoteRenderService, userId, sessionId); return new InlineFillUi(focusedViewId, inlineSuggestions, filterText); } diff --git a/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionFactory.java b/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionFactory.java index 462ffd668e2e..8fcb8aa9393c 100644 --- a/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionFactory.java +++ b/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionFactory.java @@ -53,9 +53,9 @@ final class InlineSuggestionFactory { public static InlineSuggestion createInlineAuthentication( @NonNull InlineSuggestionsRequest request, @NonNull FillResponse response, - @NonNull AutofillId autofillId, @NonNull AutoFillUI.AutoFillUiCallback client, @NonNull Runnable onErrorCallback, - @Nullable RemoteInlineSuggestionRenderService remoteRenderService) { + @Nullable RemoteInlineSuggestionRenderService remoteRenderService, int userId, + int sessionId) { final BiConsumer<Dataset, Integer> onClickFactory = (dataset, datasetIndex) -> { client.authenticate(response.getRequestId(), datasetIndex, response.getAuthentication(), response.getClientState(), @@ -66,7 +66,8 @@ final class InlineSuggestionFactory { InlinePresentation inlineAuthentication = response.getInlinePresentation(); return createInlineAuthSuggestion( mergedInlinePresentation(request, 0, inlineAuthentication), - remoteRenderService, onClickFactory, onErrorCallback, intentSenderConsumer, + remoteRenderService, userId, sessionId, + onClickFactory, onErrorCallback, intentSenderConsumer, request.getHostInputToken(), request.getHostDisplayId()); } @@ -80,7 +81,8 @@ final class InlineSuggestionFactory { @NonNull List<Dataset> datasets, @NonNull AutofillId autofillId, @NonNull AutoFillUI.AutoFillUiCallback client, @NonNull Runnable onErrorCallback, - @Nullable RemoteInlineSuggestionRenderService remoteRenderService) { + @Nullable RemoteInlineSuggestionRenderService remoteRenderService, + int userId, int sessionId) { if (sDebug) Slog.d(TAG, "createInlineSuggestionsResponse called"); final Consumer<IntentSender> intentSenderConsumer = (intentSender) -> client.startIntentSender(intentSender, new Intent()); @@ -90,7 +92,8 @@ final class InlineSuggestionFactory { return createInlineSuggestionsInternal(/* isAugmented= */ false, request, datasets, autofillId, - onErrorCallback, onClickFactory, intentSenderConsumer, remoteRenderService); + onErrorCallback, onClickFactory, intentSenderConsumer, remoteRenderService, userId, + sessionId); } /** @@ -104,7 +107,8 @@ final class InlineSuggestionFactory { @NonNull AutofillId autofillId, @NonNull InlineFillUi.InlineSuggestionUiCallback inlineSuggestionUiCallback, @NonNull Runnable onErrorCallback, - @Nullable RemoteInlineSuggestionRenderService remoteRenderService) { + @Nullable RemoteInlineSuggestionRenderService remoteRenderService, + int userId, int sessionId) { if (sDebug) Slog.d(TAG, "createAugmentedInlineSuggestionsResponse called"); return createInlineSuggestionsInternal(/* isAugmented= */ true, request, datasets, autofillId, onErrorCallback, @@ -112,7 +116,7 @@ final class InlineSuggestionFactory { inlineSuggestionUiCallback.autofill(dataset, datasetIndex), (intentSender) -> inlineSuggestionUiCallback.startIntentSender(intentSender, new Intent()), - remoteRenderService); + remoteRenderService, userId, sessionId); } @Nullable @@ -121,7 +125,8 @@ final class InlineSuggestionFactory { @NonNull List<Dataset> datasets, @NonNull AutofillId autofillId, @NonNull Runnable onErrorCallback, @NonNull BiConsumer<Dataset, Integer> onClickFactory, @NonNull Consumer<IntentSender> intentSenderConsumer, - @Nullable RemoteInlineSuggestionRenderService remoteRenderService) { + @Nullable RemoteInlineSuggestionRenderService remoteRenderService, + int userId, int sessionId) { SparseArray<Pair<Dataset, InlineSuggestion>> response = new SparseArray<>(datasets.size()); for (int datasetIndex = 0; datasetIndex < datasets.size(); datasetIndex++) { final Dataset dataset = datasets.get(datasetIndex); @@ -139,7 +144,8 @@ final class InlineSuggestionFactory { InlineSuggestion inlineSuggestion = createInlineSuggestion(isAugmented, dataset, datasetIndex, mergedInlinePresentation(request, datasetIndex, inlinePresentation), - onClickFactory, remoteRenderService, onErrorCallback, intentSenderConsumer, + onClickFactory, remoteRenderService, userId, sessionId, + onErrorCallback, intentSenderConsumer, request.getHostInputToken(), request.getHostDisplayId()); response.append(datasetIndex, Pair.create(dataset, inlineSuggestion)); } @@ -151,6 +157,7 @@ final class InlineSuggestionFactory { @NonNull InlinePresentation inlinePresentation, @NonNull BiConsumer<Dataset, Integer> onClickFactory, @NonNull RemoteInlineSuggestionRenderService remoteRenderService, + int userId, int sessionId, @NonNull Runnable onErrorCallback, @NonNull Consumer<IntentSender> intentSenderConsumer, @Nullable IBinder hostInputToken, int displayId) { @@ -167,7 +174,8 @@ final class InlineSuggestionFactory { final InlineSuggestion inlineSuggestion = new InlineSuggestion(inlineSuggestionInfo, createInlineContentProvider(inlinePresentation, () -> onClickFactory.accept(dataset, datasetIndex), onErrorCallback, - intentSenderConsumer, remoteRenderService, hostInputToken, displayId)); + intentSenderConsumer, remoteRenderService, userId, sessionId, + hostInputToken, displayId)); return inlineSuggestion; } @@ -175,6 +183,7 @@ final class InlineSuggestionFactory { private static InlineSuggestion createInlineAuthSuggestion( @NonNull InlinePresentation inlinePresentation, @NonNull RemoteInlineSuggestionRenderService remoteRenderService, + int userId, int sessionId, @NonNull BiConsumer<Dataset, Integer> onClickFactory, @NonNull Runnable onErrorCallback, @NonNull Consumer<IntentSender> intentSenderConsumer, @Nullable IBinder hostInputToken, int displayId) { @@ -187,8 +196,8 @@ final class InlineSuggestionFactory { createInlineContentProvider(inlinePresentation, () -> onClickFactory.accept(null, AutofillManager.AUTHENTICATION_ID_DATASET_ID_UNDEFINED), - onErrorCallback, intentSenderConsumer, remoteRenderService, hostInputToken, - displayId)); + onErrorCallback, intentSenderConsumer, remoteRenderService, userId, + sessionId, hostInputToken, displayId)); } /** @@ -216,12 +225,13 @@ final class InlineSuggestionFactory { @NonNull Runnable onErrorCallback, @NonNull Consumer<IntentSender> intentSenderConsumer, @Nullable RemoteInlineSuggestionRenderService remoteRenderService, + int userId, int sessionId, @Nullable IBinder hostInputToken, int displayId) { RemoteInlineSuggestionViewConnector remoteInlineSuggestionViewConnector = new RemoteInlineSuggestionViewConnector( - remoteRenderService, inlinePresentation, hostInputToken, displayId, onClickAction, - onErrorCallback, intentSenderConsumer); + remoteRenderService, userId, sessionId, inlinePresentation, hostInputToken, + displayId, onClickAction, onErrorCallback, intentSenderConsumer); InlineContentProviderImpl inlineContentProvider = new InlineContentProviderImpl( remoteInlineSuggestionViewConnector, null); return inlineContentProvider; diff --git a/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionViewConnector.java b/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionViewConnector.java index 9d23c171800d..7257255d1ee4 100644 --- a/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionViewConnector.java +++ b/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionViewConnector.java @@ -46,6 +46,8 @@ final class RemoteInlineSuggestionViewConnector { @Nullable private final IBinder mHostInputToken; private final int mDisplayId; + private final int mUserId; + private final int mSessionId; @NonNull private final Runnable mOnAutofillCallback; @@ -56,6 +58,7 @@ final class RemoteInlineSuggestionViewConnector { RemoteInlineSuggestionViewConnector( @Nullable RemoteInlineSuggestionRenderService remoteRenderService, + int userId, int sessionId, @NonNull InlinePresentation inlinePresentation, @Nullable IBinder hostInputToken, int displayId, @@ -66,6 +69,8 @@ final class RemoteInlineSuggestionViewConnector { mInlinePresentation = inlinePresentation; mHostInputToken = hostInputToken; mDisplayId = displayId; + mUserId = userId; + mSessionId = sessionId; mOnAutofillCallback = onAutofillCallback; mOnErrorCallback = onErrorCallback; @@ -82,7 +87,7 @@ final class RemoteInlineSuggestionViewConnector { if (mRemoteRenderService != null) { if (sDebug) Slog.d(TAG, "Request to recreate the UI"); mRemoteRenderService.renderSuggestion(callback, mInlinePresentation, width, height, - mHostInputToken, mDisplayId); + mHostInputToken, mDisplayId, mUserId, mSessionId); return true; } return false; diff --git a/services/core/Android.bp b/services/core/Android.bp index 65e98ac8e684..4bba0d892f3b 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -95,7 +95,6 @@ java_library_static { "android.hardware.light-V2.0-java", "android.hardware.power-java", "android.hardware.power-V1.0-java", - "android.hardware.tv.cec-V1.0-java", "android.hardware.vibrator-java", "android.net.ipsec.ike.stubs.module_lib", "app-compat-annotations", @@ -117,6 +116,7 @@ java_library_static { "android.hardware.health-V2.0-java", "android.hardware.health-V2.1-java", "android.hardware.light-java", + "android.hardware.tv.cec-V1.0-java", "android.hardware.weaver-V1.0-java", "android.hardware.biometrics.face-V1.0-java", "android.hardware.biometrics.fingerprint-V2.2-java", diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java index 4ff421e6cdf5..135ac9a7846e 100644 --- a/services/core/java/com/android/server/am/ActivityManagerConstants.java +++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java @@ -87,6 +87,7 @@ final class ActivityManagerConstants extends ContentObserver { static final String KEY_PROCESS_START_ASYNC = "process_start_async"; static final String KEY_MEMORY_INFO_THROTTLE_TIME = "memory_info_throttle_time"; static final String KEY_TOP_TO_FGS_GRACE_DURATION = "top_to_fgs_grace_duration"; + static final String KEY_PENDINGINTENT_WARNING_THRESHOLD = "pendingintent_warning_threshold"; private static final int DEFAULT_MAX_CACHED_PROCESSES = 32; private static final long DEFAULT_BACKGROUND_SETTLE_TIME = 60*1000; @@ -119,6 +120,7 @@ final class ActivityManagerConstants extends ContentObserver { private static final boolean DEFAULT_PROCESS_START_ASYNC = true; private static final long DEFAULT_MEMORY_INFO_THROTTLE_TIME = 5*60*1000; private static final long DEFAULT_TOP_TO_FGS_GRACE_DURATION = 15 * 1000; + private static final int DEFAULT_PENDINGINTENT_WARNING_THRESHOLD = 2000; // Flag stored in the DeviceConfig API. /** @@ -328,6 +330,12 @@ final class ActivityManagerConstants extends ContentObserver { */ public ArraySet<Integer> IMPERCEPTIBLE_KILL_EXEMPT_PROC_STATES = new ArraySet<Integer>(); + /** + * The threshold for the amount of PendingIntent for each UID, there will be + * warning logs if the number goes beyond this threshold. + */ + public int PENDINGINTENT_WARNING_THRESHOLD = DEFAULT_PENDINGINTENT_WARNING_THRESHOLD; + private List<String> mDefaultImperceptibleKillExemptPackages; private List<Integer> mDefaultImperceptibleKillExemptProcStates; @@ -562,6 +570,8 @@ final class ActivityManagerConstants extends ContentObserver { DEFAULT_MEMORY_INFO_THROTTLE_TIME); TOP_TO_FGS_GRACE_DURATION = mParser.getDurationMillis(KEY_TOP_TO_FGS_GRACE_DURATION, DEFAULT_TOP_TO_FGS_GRACE_DURATION); + PENDINGINTENT_WARNING_THRESHOLD = mParser.getInt(KEY_PENDINGINTENT_WARNING_THRESHOLD, + DEFAULT_PENDINGINTENT_WARNING_THRESHOLD); // For new flags that are intended for server-side experiments, please use the new // DeviceConfig package. diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 157feb30c24d..e77b361c8c06 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -82,8 +82,6 @@ import static android.os.Process.removeAllProcessGroups; import static android.os.Process.sendSignal; import static android.os.Process.setThreadPriority; import static android.os.Process.setThreadScheduler; -import static android.permission.PermissionManager.KILL_APP_REASON_GIDS_CHANGED; -import static android.permission.PermissionManager.KILL_APP_REASON_PERMISSIONS_REVOKED; import static android.provider.Settings.Global.ALWAYS_FINISH_ACTIVITIES; import static android.provider.Settings.Global.DEBUG_APP; import static android.provider.Settings.Global.NETWORK_ACCESS_TIMEOUT_MS; @@ -1669,12 +1667,6 @@ public class ActivityManagerService extends IActivityManager.Stub */ @Nullable ContentCaptureManagerInternal mContentCaptureService; - /** - * Set of {@link ProcessRecord} that have either {@link ProcessRecord#hasTopUi()} or - * {@link ProcessRecord#runningRemoteAnimation} set to {@code true}. - */ - final ArraySet<ProcessRecord> mTopUiOrRunningRemoteAnimApps = new ArraySet<>(); - final class UiHandler extends Handler { public UiHandler() { super(com.android.server.UiThread.get().getLooper(), null, true); @@ -2543,7 +2535,8 @@ public class ActivityManagerService extends IActivityManager.Stub mUiHandler = injector.getUiHandler(null /* service */); mUserController = hasHandlerThread ? new UserController(this) : null; mPendingIntentController = hasHandlerThread - ? new PendingIntentController(handlerThread.getLooper(), mUserController) : null; + ? new PendingIntentController(handlerThread.getLooper(), mUserController, + mConstants) : null; mProcStartHandlerThread = null; mProcStartHandler = null; mHiddenApiBlacklist = null; @@ -2640,7 +2633,7 @@ public class ActivityManagerService extends IActivityManager.Stub mUserController = new UserController(this); mPendingIntentController = new PendingIntentController( - mHandlerThread.getLooper(), mUserController); + mHandlerThread.getLooper(), mUserController, mConstants); if (SystemProperties.getInt("sys.use_fifo_ui", 0) != 0) { mUseFifoUiScheduling = true; @@ -9201,16 +9194,31 @@ public class ActivityManagerService extends IActivityManager.Stub synchronized (this) { final long identity = Binder.clearCallingIdentity(); try { - boolean permissionChange = KILL_APP_REASON_PERMISSIONS_REVOKED.equals(reason) - || KILL_APP_REASON_GIDS_CHANGED.equals(reason); mProcessList.killPackageProcessesLocked(null /* packageName */, appId, userId, ProcessList.PERSISTENT_PROC_ADJ, false /* callerWillRestart */, true /* callerWillRestart */, true /* doit */, true /* evenPersistent */, false /* setRemoved */, - permissionChange ? ApplicationExitInfo.REASON_PERMISSION_CHANGE - : ApplicationExitInfo.REASON_OTHER, - permissionChange ? ApplicationExitInfo.SUBREASON_UNKNOWN - : ApplicationExitInfo.SUBREASON_KILL_UID, + ApplicationExitInfo.REASON_OTHER, + ApplicationExitInfo.SUBREASON_KILL_UID, + reason != null ? reason : "kill uid"); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + + @Override + public void killUidForPermissionChange(int appId, int userId, String reason) { + enforceCallingPermission(Manifest.permission.KILL_UID, "killUid"); + synchronized (this) { + final long identity = Binder.clearCallingIdentity(); + try { + mProcessList.killPackageProcessesLocked(null /* packageName */, appId, userId, + ProcessList.PERSISTENT_PROC_ADJ, false /* callerWillRestart */, + true /* callerWillRestart */, true /* doit */, true /* evenPersistent */, + false /* setRemoved */, + ApplicationExitInfo.REASON_PERMISSION_CHANGE, + ApplicationExitInfo.SUBREASON_UNKNOWN, reason != null ? reason : "kill uid"); } finally { Binder.restoreCallingIdentity(identity); @@ -14706,7 +14714,6 @@ public class ActivityManagerService extends IActivityManager.Stub mProcessesToGc.remove(app); mPendingPssProcesses.remove(app); - mTopUiOrRunningRemoteAnimApps.remove(app); ProcessList.abortNextPssTime(app.procStateMemTracker); // Dismiss any open dialogs. @@ -18507,22 +18514,6 @@ public class ActivityManagerService extends IActivityManager.Stub return proc; } - /** - * @return {@code true} if {@link #mTopUiOrRunningRemoteAnimApps} set contains {@code app} or when there are no apps - * in this list, an false otherwise. - */ - boolean containsTopUiOrRunningRemoteAnimOrEmptyLocked(ProcessRecord app) { - return mTopUiOrRunningRemoteAnimApps.isEmpty() || mTopUiOrRunningRemoteAnimApps.contains(app); - } - - void addTopUiOrRunningRemoteAnim(ProcessRecord app) { - mTopUiOrRunningRemoteAnimApps.add(app); - } - - void removeTopUiOrRunningRemoteAnim(ProcessRecord app) { - mTopUiOrRunningRemoteAnimApps.remove(app); - } - @Override public boolean dumpHeap(String process, int userId, boolean managed, boolean mallocInfo, boolean runGc, String path, ParcelFileDescriptor fd, RemoteCallback finishCallback) { diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index 58b0a157e2c2..da5f48962130 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -1151,17 +1151,8 @@ public final class OomAdjuster { // is currently showing UI. app.systemNoUi = true; if (app == topApp) { - // If specific system app has set ProcessRecord.mHasTopUi or is running a remote - // animation (ProcessRecord.runningRemoteAnimation), this will prevent topApp - // to use SCHED_GROUP_TOP_APP to ensure process with mHasTopUi will have exclusive - // access to configured cores. - if (mService.containsTopUiOrRunningRemoteAnimOrEmptyLocked(app)) { - app.setCurrentSchedulingGroup(ProcessList.SCHED_GROUP_TOP_APP); - } else { - app.setCurrentSchedulingGroup(ProcessList.SCHED_GROUP_DEFAULT); - } app.systemNoUi = false; - + app.setCurrentSchedulingGroup(ProcessList.SCHED_GROUP_TOP_APP); app.adjType = "pers-top-activity"; } else if (app.hasTopUi()) { // sched group/proc state adjustment is below @@ -1202,20 +1193,10 @@ public final class OomAdjuster { boolean foregroundActivities = false; if (PROCESS_STATE_CUR_TOP == PROCESS_STATE_TOP && app == topApp) { - - // If specific system app has set ProcessRecord.mHasTopUi or is running a remote - // animation (ProcessRecord.runningRemoteAnimation), this will prevent topApp - // to use SCHED_GROUP_TOP_APP to ensure process with mHasTopUi will have exclusive - // access to configured cores. - if (mService.containsTopUiOrRunningRemoteAnimOrEmptyLocked(app)) { - adj = ProcessList.FOREGROUND_APP_ADJ; - schedGroup = ProcessList.SCHED_GROUP_TOP_APP; - app.adjType = "top-activity"; - } else { - adj = ProcessList.FOREGROUND_APP_ADJ; - schedGroup = ProcessList.SCHED_GROUP_DEFAULT; - app.adjType = "top-activity-behind-topui"; - } + // The last app on the list is the foreground app. + adj = ProcessList.FOREGROUND_APP_ADJ; + schedGroup = ProcessList.SCHED_GROUP_TOP_APP; + app.adjType = "top-activity"; foregroundActivities = true; procState = PROCESS_STATE_CUR_TOP; if (DEBUG_OOM_ADJ_REASON || logUid == appUid) { diff --git a/services/core/java/com/android/server/am/PendingIntentController.java b/services/core/java/com/android/server/am/PendingIntentController.java index eacf08889b1d..c62df56a88c5 100644 --- a/services/core/java/com/android/server/am/PendingIntentController.java +++ b/services/core/java/com/android/server/am/PendingIntentController.java @@ -41,8 +41,12 @@ import android.os.RemoteException; import android.os.UserHandle; import android.util.ArrayMap; import android.util.Slog; +import android.util.SparseArray; +import android.util.SparseIntArray; +import com.android.internal.annotations.GuardedBy; import com.android.internal.os.IResultReceiver; +import com.android.internal.util.RingBuffer; import com.android.internal.util.function.pooled.PooledLambda; import com.android.server.AlarmManagerInternal; import com.android.server.LocalServices; @@ -52,6 +56,7 @@ import com.android.server.wm.SafeActivityOptions; import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; @@ -66,6 +71,9 @@ public class PendingIntentController { private static final String TAG = TAG_WITH_CLASS_NAME ? "PendingIntentController" : TAG_AM; private static final String TAG_MU = TAG + POSTFIX_MU; + /** @see {@link #mRecentIntentsPerUid}. */ + private static final int RECENT_N = 10; + /** Lock for internal state. */ final Object mLock = new Object(); final Handler mH; @@ -77,10 +85,22 @@ public class PendingIntentController { final HashMap<PendingIntentRecord.Key, WeakReference<PendingIntentRecord>> mIntentSenderRecords = new HashMap<>(); - PendingIntentController(Looper looper, UserController userController) { + /** The number of PendingIntentRecord per uid */ + @GuardedBy("mLock") + private final SparseIntArray mIntentsPerUid = new SparseIntArray(); + + /** The recent PendingIntentRecord, up to {@link #RECENT_N} per uid */ + @GuardedBy("mLock") + private final SparseArray<RingBuffer<String>> mRecentIntentsPerUid = new SparseArray<>(); + + private final ActivityManagerConstants mConstants; + + PendingIntentController(Looper looper, UserController userController, + ActivityManagerConstants constants) { mH = new Handler(looper); mAtmInternal = LocalServices.getService(ActivityTaskManagerInternal.class); mUserController = userController; + mConstants = constants; } void onActivityManagerInternalAdded() { @@ -136,12 +156,14 @@ public class PendingIntentController { } makeIntentSenderCanceled(rec); mIntentSenderRecords.remove(key); + decrementUidStatLocked(rec); } if (noCreate) { return rec; } rec = new PendingIntentRecord(this, key, callingUid); mIntentSenderRecords.put(key, rec.ref); + incrementUidStatLocked(rec); return rec; } } @@ -198,6 +220,7 @@ public class PendingIntentController { didSomething = true; it.remove(); makeIntentSenderCanceled(pir); + decrementUidStatLocked(pir); if (pir.key.activity != null) { final Message m = PooledLambda.obtainMessage( PendingIntentController::clearPendingResultForActivity, this, @@ -237,6 +260,7 @@ public class PendingIntentController { synchronized (mLock) { makeIntentSenderCanceled(rec); mIntentSenderRecords.remove(rec.key); + decrementUidStatLocked(rec); if (cleanActivity && rec.key.activity != null) { final Message m = PooledLambda.obtainMessage( PendingIntentController::clearPendingResultForActivity, this, @@ -369,9 +393,81 @@ public class PendingIntentController { } } + final int sizeOfIntentsPerUid = mIntentsPerUid.size(); + if (sizeOfIntentsPerUid > 0) { + for (int i = 0; i < sizeOfIntentsPerUid; i++) { + pw.print(" * UID: "); + pw.print(mIntentsPerUid.keyAt(i)); + pw.print(" total: "); + pw.println(mIntentsPerUid.valueAt(i)); + } + } + if (!printed) { pw.println(" (nothing)"); } } } + + /** + * Increment the number of the PendingIntentRecord for the given uid, log a warning + * if there are too many for this uid already. + */ + @GuardedBy("mLock") + void incrementUidStatLocked(final PendingIntentRecord pir) { + final int uid = pir.uid; + final int idx = mIntentsPerUid.indexOfKey(uid); + int newCount = 1; + if (idx >= 0) { + newCount = mIntentsPerUid.valueAt(idx) + 1; + mIntentsPerUid.setValueAt(idx, newCount); + } else { + mIntentsPerUid.put(uid, newCount); + } + + // If the number is within the range [threshold - N + 1, threshold], log it into buffer + final int lowBound = mConstants.PENDINGINTENT_WARNING_THRESHOLD - RECENT_N + 1; + RingBuffer<String> recentHistory = null; + if (newCount == lowBound) { + recentHistory = new RingBuffer(String.class, RECENT_N); + mRecentIntentsPerUid.put(uid, recentHistory); + } else if (newCount > lowBound && newCount <= mConstants.PENDINGINTENT_WARNING_THRESHOLD) { + recentHistory = mRecentIntentsPerUid.get(uid); + } + if (recentHistory == null) { + return; + } + + recentHistory.append(pir.key.toString()); + + // Output the log if we are hitting the threshold + if (newCount == mConstants.PENDINGINTENT_WARNING_THRESHOLD) { + Slog.wtf(TAG, "Too many PendingIntent created for uid " + uid + + ", recent " + RECENT_N + ": " + Arrays.toString(recentHistory.toArray())); + // Clear the buffer, as we don't want to spam the log when the numbers + // are jumping up and down around the threshold. + mRecentIntentsPerUid.remove(uid); + } + } + + /** + * Decrement the number of the PendingIntentRecord for the given uid. + */ + @GuardedBy("mLock") + void decrementUidStatLocked(final PendingIntentRecord pir) { + final int uid = pir.uid; + final int idx = mIntentsPerUid.indexOfKey(uid); + if (idx >= 0) { + final int newCount = mIntentsPerUid.valueAt(idx) - 1; + // If we are going below the low threshold, no need to keep logs. + if (newCount == mConstants.PENDINGINTENT_WARNING_THRESHOLD - RECENT_N) { + mRecentIntentsPerUid.delete(uid); + } + if (newCount == 0) { + mIntentsPerUid.removeAt(idx); + } else { + mIntentsPerUid.setValueAt(idx, newCount); + } + } + } } diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java index d54d2d7d2056..1997dbd6fc37 100644 --- a/services/core/java/com/android/server/am/PendingIntentRecord.java +++ b/services/core/java/com/android/server/am/PendingIntentRecord.java @@ -187,7 +187,8 @@ public final class PendingIntentRecord extends IIntentSender.Stub { + " intent=" + (requestIntent != null ? requestIntent.toShortString(false, true, false, false) : "<null>") - + " flags=0x" + Integer.toHexString(flags) + " u=" + userId + "}"; + + " flags=0x" + Integer.toHexString(flags) + " u=" + userId + "}" + + " requestCode=" + requestCode; } String typeName() { @@ -499,6 +500,7 @@ public final class PendingIntentRecord extends IIntentSender.Stub { WeakReference<PendingIntentRecord> current = controller.mIntentSenderRecords.get(key); if (current == ref) { controller.mIntentSenderRecords.remove(key); + controller.decrementUidStatLocked(this); } } } diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java index 4c75ab21d6f2..c5152c081e70 100644 --- a/services/core/java/com/android/server/am/ProcessRecord.java +++ b/services/core/java/com/android/server/am/ProcessRecord.java @@ -1268,7 +1268,6 @@ class ProcessRecord implements WindowProcessListener { void setHasTopUi(boolean hasTopUi) { mHasTopUi = hasTopUi; mWindowProcessController.setHasTopUi(hasTopUi); - updateTopUiOrRunningRemoteAnim(); } boolean hasTopUi() { @@ -1519,19 +1518,10 @@ class ProcessRecord implements WindowProcessListener { Slog.i(TAG, "Setting runningRemoteAnimation=" + runningRemoteAnimation + " for pid=" + pid); } - updateTopUiOrRunningRemoteAnim(); mService.updateOomAdjLocked(this, true, OomAdjuster.OOM_ADJ_REASON_UI_VISIBILITY); } } - void updateTopUiOrRunningRemoteAnim() { - if (runningRemoteAnimation || hasTopUi()) { - mService.addTopUiOrRunningRemoteAnim(this); - } else { - mService.removeTopUiOrRunningRemoteAnim(this); - } - } - public long getInputDispatchingTimeout() { return mWindowProcessController.getInputDispatchingTimeout(); } diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index f8ab6f4a8b54..5e908b26fafa 100755 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -422,6 +422,9 @@ public class AudioService extends IAudioService.Stub private final boolean mUseFixedVolume; + // If absolute volume is supported in AVRCP device + private volatile boolean mAvrcpAbsVolSupported = false; + /** * Default stream type used for volume control in the absence of playback * e.g. user on homescreen, no app playing anything, presses hardware volume buttons, this @@ -4994,7 +4997,7 @@ public class AudioService extends IAudioService.Stub return AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_MULTI_MODE; } if (audioSystemDeviceOut == AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP - && mDeviceBroker.isAvrcpAbsoluteVolumeSupported()) { + && mAvrcpAbsVolSupported) { return AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE; } return AudioManager.DEVICE_VOLUME_BEHAVIOR_VARIABLE; @@ -5687,12 +5690,12 @@ public class AudioService extends IAudioService.Stub } // must be called while synchronized VolumeStreamState.class - /*package*/ void applyDeviceVolume_syncVSS(int device, boolean isAvrcpAbsVolSupported) { + /*package*/ void applyDeviceVolume_syncVSS(int device) { int index; if (isFullyMuted()) { index = 0; } else if (AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device) - && isAvrcpAbsVolSupported) { + && mAvrcpAbsVolSupported) { index = getAbsoluteVolumeIndex((getIndex(device) + 5)/10); } else if (isFullVolumeDevice(device)) { index = (mIndexMax + 5)/10; @@ -5705,7 +5708,6 @@ public class AudioService extends IAudioService.Stub } public void applyAllVolumes() { - final boolean isAvrcpAbsVolSupported = mDeviceBroker.isAvrcpAbsoluteVolumeSupported(); synchronized (VolumeStreamState.class) { // apply device specific volumes first int index; @@ -5715,7 +5717,7 @@ public class AudioService extends IAudioService.Stub if (isFullyMuted()) { index = 0; } else if (AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device) - && isAvrcpAbsVolSupported) { + && mAvrcpAbsVolSupported) { index = getAbsoluteVolumeIndex((getIndex(device) + 5)/10); } else if (isFullVolumeDevice(device)) { index = (mIndexMax + 5)/10; @@ -5955,7 +5957,6 @@ public class AudioService extends IAudioService.Stub } public void checkFixedVolumeDevices() { - final boolean isAvrcpAbsVolSupported = mDeviceBroker.isAvrcpAbsoluteVolumeSupported(); synchronized (VolumeStreamState.class) { // ignore settings for fixed volume devices: volume should always be at max or 0 if (mStreamVolumeAlias[mStreamType] == AudioSystem.STREAM_MUSIC) { @@ -5966,7 +5967,7 @@ public class AudioService extends IAudioService.Stub || (isFixedVolumeDevice(device) && index != 0)) { mIndexMap.put(device, mIndexMax); } - applyDeviceVolume_syncVSS(device, isAvrcpAbsVolSupported); + applyDeviceVolume_syncVSS(device); } } } @@ -6130,11 +6131,9 @@ public class AudioService extends IAudioService.Stub /*package*/ void setDeviceVolume(VolumeStreamState streamState, int device) { - final boolean isAvrcpAbsVolSupported = mDeviceBroker.isAvrcpAbsoluteVolumeSupported(); - synchronized (VolumeStreamState.class) { // Apply volume - streamState.applyDeviceVolume_syncVSS(device, isAvrcpAbsVolSupported); + streamState.applyDeviceVolume_syncVSS(device); // Apply change to all streams using this one as alias int numStreamTypes = AudioSystem.getNumStreamTypes(); @@ -6144,13 +6143,11 @@ public class AudioService extends IAudioService.Stub // Make sure volume is also maxed out on A2DP device for aliased stream // that may have a different device selected int streamDevice = getDeviceForStream(streamType); - if ((device != streamDevice) && isAvrcpAbsVolSupported + if ((device != streamDevice) && mAvrcpAbsVolSupported && AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device)) { - mStreamStates[streamType].applyDeviceVolume_syncVSS(device, - isAvrcpAbsVolSupported); + mStreamStates[streamType].applyDeviceVolume_syncVSS(device); } - mStreamStates[streamType].applyDeviceVolume_syncVSS(streamDevice, - isAvrcpAbsVolSupported); + mStreamStates[streamType].applyDeviceVolume_syncVSS(streamDevice); } } } @@ -6458,6 +6455,7 @@ public class AudioService extends IAudioService.Stub // address is not used for now, but may be used when multiple a2dp devices are supported sVolumeLogger.log(new AudioEventLogger.StringEvent("avrcpSupportsAbsoluteVolume addr=" + address + " support=" + support)); + mAvrcpAbsVolSupported = support; mDeviceBroker.setAvrcpAbsoluteVolumeSupported(support); sendMsg(mAudioHandler, MSG_SET_DEVICE_VOLUME, SENDMSG_QUEUE, AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, 0, @@ -7415,8 +7413,7 @@ public class AudioService extends IAudioService.Stub pw.print(" mCameraSoundForced="); pw.println(mCameraSoundForced); pw.print(" mHasVibrator="); pw.println(mHasVibrator); pw.print(" mVolumePolicy="); pw.println(mVolumePolicy); - pw.print(" mAvrcpAbsVolSupported="); - pw.println(mDeviceBroker.isAvrcpAbsoluteVolumeSupported()); + pw.print(" mAvrcpAbsVolSupported="); pw.println(mAvrcpAbsVolSupported); pw.print(" mIsSingleVolume="); pw.println(mIsSingleVolume); pw.print(" mUseFixedVolume="); pw.println(mUseFixedVolume); pw.print(" mFixedVolumeDevices="); pw.println(dumpDeviceTypes(mFixedVolumeDevices)); diff --git a/services/core/java/com/android/server/hdmi/HdmiCecController.java b/services/core/java/com/android/server/hdmi/HdmiCecController.java index b84d3226362b..75ab33dbbfc7 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecController.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecController.java @@ -17,11 +17,18 @@ package com.android.server.hdmi; import android.hardware.hdmi.HdmiPortInfo; +import android.hardware.tv.cec.V1_0.CecMessage; +import android.hardware.tv.cec.V1_0.HotplugEvent; +import android.hardware.tv.cec.V1_0.IHdmiCec; +import android.hardware.tv.cec.V1_0.IHdmiCec.getPhysicalAddressCallback; +import android.hardware.tv.cec.V1_0.IHdmiCecCallback; import android.hardware.tv.cec.V1_0.Result; import android.hardware.tv.cec.V1_0.SendMessageResult; import android.os.Handler; +import android.os.IHwBinder; import android.os.Looper; import android.os.MessageQueue; +import android.os.RemoteException; import android.os.SystemProperties; import android.util.Slog; import android.util.SparseArray; @@ -79,6 +86,11 @@ final class HdmiCecController { private static final int MAX_HDMI_MESSAGE_HISTORY = 250; + private static final int INVALID_PHYSICAL_ADDRESS = 0xFFFF; + + /** Cookie for matching the right end point. */ + protected static final int HDMI_CEC_HAL_DEATH_COOKIE = 353; + // Predicate for whether the given logical address is remote device's one or not. private final Predicate<Integer> mRemoteDeviceAddressPredicate = new Predicate<Integer>() { @Override @@ -102,10 +114,6 @@ final class HdmiCecController { // device or issued by internal state change. private Handler mControlHandler; - // Stores the pointer to the native implementation of the service that - // interacts with HAL. - private volatile long mNativePtr; - private final HdmiControlService mService; // Stores the local CEC devices in the system. Device type is used for key. @@ -149,23 +157,21 @@ final class HdmiCecController { * A factory method with injection of native methods for testing. */ static HdmiCecController createWithNativeWrapper( - HdmiControlService service, NativeWrapper nativeWrapper) { - HdmiCecController controller = new HdmiCecController(service, nativeWrapper); - long nativePtr = nativeWrapper - .nativeInit(controller, service.getServiceLooper().getQueue()); - if (nativePtr == 0L) { - controller = null; - return null; - } - - controller.init(nativePtr); - return controller; + HdmiControlService service, NativeWrapper nativeWrapper) { + HdmiCecController controller = new HdmiCecController(service, nativeWrapper); + String nativePtr = nativeWrapper.nativeInit(); + if (nativePtr == null) { + HdmiLogger.warning("Couldn't get tv.cec service."); + return null; + } + controller.init(nativeWrapper); + return controller; } - private void init(long nativePtr) { + private void init(NativeWrapper nativeWrapper) { mIoHandler = new Handler(mService.getIoLooper()); mControlHandler = new Handler(mService.getServiceLooper()); - mNativePtr = nativePtr; + nativeWrapper.setCallback(new HdmiCecCallback()); } @ServiceThreadOnly @@ -261,7 +267,7 @@ final class HdmiCecController { HdmiPortInfo[] getPortInfos() { - return mNativeWrapperImpl.nativeGetPortInfos(mNativePtr); + return mNativeWrapperImpl.nativeGetPortInfos(); } /** @@ -289,7 +295,7 @@ final class HdmiCecController { int addLogicalAddress(int newLogicalAddress) { assertRunOnServiceThread(); if (HdmiUtils.isValidAddress(newLogicalAddress)) { - return mNativeWrapperImpl.nativeAddLogicalAddress(mNativePtr, newLogicalAddress); + return mNativeWrapperImpl.nativeAddLogicalAddress(newLogicalAddress); } else { return Result.FAILURE_INVALID_ARGS; } @@ -306,7 +312,7 @@ final class HdmiCecController { for (int i = 0; i < mLocalDevices.size(); ++i) { mLocalDevices.valueAt(i).clearAddress(); } - mNativeWrapperImpl.nativeClearLogicalAddress(mNativePtr); + mNativeWrapperImpl.nativeClearLogicalAddress(); } @ServiceThreadOnly @@ -326,7 +332,7 @@ final class HdmiCecController { @ServiceThreadOnly int getPhysicalAddress() { assertRunOnServiceThread(); - return mNativeWrapperImpl.nativeGetPhysicalAddress(mNativePtr); + return mNativeWrapperImpl.nativeGetPhysicalAddress(); } /** @@ -337,7 +343,7 @@ final class HdmiCecController { @ServiceThreadOnly int getVersion() { assertRunOnServiceThread(); - return mNativeWrapperImpl.nativeGetVersion(mNativePtr); + return mNativeWrapperImpl.nativeGetVersion(); } /** @@ -348,7 +354,7 @@ final class HdmiCecController { @ServiceThreadOnly int getVendorId() { assertRunOnServiceThread(); - return mNativeWrapperImpl.nativeGetVendorId(mNativePtr); + return mNativeWrapperImpl.nativeGetVendorId(); } /** @@ -361,7 +367,7 @@ final class HdmiCecController { void setOption(int flag, boolean enabled) { assertRunOnServiceThread(); HdmiLogger.debug("setOption: [flag:%d, enabled:%b]", flag, enabled); - mNativeWrapperImpl.nativeSetOption(mNativePtr, flag, enabled); + mNativeWrapperImpl.nativeSetOption(flag, enabled); } /** @@ -375,7 +381,7 @@ final class HdmiCecController { if (!LanguageTag.isLanguage(language)) { return; } - mNativeWrapperImpl.nativeSetLanguage(mNativePtr, language); + mNativeWrapperImpl.nativeSetLanguage(language); } /** @@ -387,7 +393,7 @@ final class HdmiCecController { @ServiceThreadOnly void enableAudioReturnChannel(int port, boolean enabled) { assertRunOnServiceThread(); - mNativeWrapperImpl.nativeEnableAudioReturnChannel(mNativePtr, port, enabled); + mNativeWrapperImpl.nativeEnableAudioReturnChannel(port, enabled); } /** @@ -399,7 +405,7 @@ final class HdmiCecController { @ServiceThreadOnly boolean isConnected(int port) { assertRunOnServiceThread(); - return mNativeWrapperImpl.nativeIsConnected(mNativePtr, port); + return mNativeWrapperImpl.nativeIsConnected(port); } /** @@ -521,7 +527,7 @@ final class HdmiCecController { // <Polling Message> is a message which has empty body. int ret = mNativeWrapperImpl.nativeSendCecCommand( - mNativePtr, sourceAddress, destinationAddress, EMPTY_BODY); + sourceAddress, destinationAddress, EMPTY_BODY); if (ret == SendMessageResult.SUCCESS) { return true; } else if (ret != SendMessageResult.NACK) { @@ -627,7 +633,7 @@ final class HdmiCecController { int i = 0; int errorCode = SendMessageResult.SUCCESS; do { - errorCode = mNativeWrapperImpl.nativeSendCecCommand(mNativePtr, + errorCode = mNativeWrapperImpl.nativeSendCecCommand( cecMessage.getSource(), cecMessage.getDestination(), body); if (errorCode == SendMessageResult.SUCCESS) { break; @@ -651,7 +657,7 @@ final class HdmiCecController { } /** - * Called by native when incoming CEC message arrived. + * Called when incoming CEC message arrived. */ @ServiceThreadOnly private void handleIncomingCecCommand(int srcAddress, int dstAddress, byte[] body) { @@ -663,7 +669,7 @@ final class HdmiCecController { } /** - * Called by native when a hotplug event issues. + * Called when a hotplug event issues. */ @ServiceThreadOnly private void handleHotplug(int port, boolean connected) { @@ -710,18 +716,19 @@ final class HdmiCecController { } protected interface NativeWrapper { - long nativeInit(HdmiCecController handler, MessageQueue messageQueue); - int nativeSendCecCommand(long controllerPtr, int srcAddress, int dstAddress, byte[] body); - int nativeAddLogicalAddress(long controllerPtr, int logicalAddress); - void nativeClearLogicalAddress(long controllerPtr); - int nativeGetPhysicalAddress(long controllerPtr); - int nativeGetVersion(long controllerPtr); - int nativeGetVendorId(long controllerPtr); - HdmiPortInfo[] nativeGetPortInfos(long controllerPtr); - void nativeSetOption(long controllerPtr, int flag, boolean enabled); - void nativeSetLanguage(long controllerPtr, String language); - void nativeEnableAudioReturnChannel(long controllerPtr, int port, boolean flag); - boolean nativeIsConnected(long controllerPtr, int port); + String nativeInit(); + void setCallback(HdmiCecCallback callback); + int nativeSendCecCommand(int srcAddress, int dstAddress, byte[] body); + int nativeAddLogicalAddress(int logicalAddress); + void nativeClearLogicalAddress(); + int nativeGetPhysicalAddress(); + int nativeGetVersion(); + int nativeGetVendorId(); + HdmiPortInfo[] nativeGetPortInfos(); + void nativeSetOption(int flag, boolean enabled); + void nativeSetLanguage(String language); + void nativeEnableAudioReturnChannel(int port, boolean flag); + boolean nativeIsConnected(int port); } private static native long nativeInit(HdmiCecController handler, MessageQueue messageQueue); @@ -739,67 +746,200 @@ final class HdmiCecController { int port, boolean flag); private static native boolean nativeIsConnected(long controllerPtr, int port); - private static final class NativeWrapperImpl implements NativeWrapper { + private static final class NativeWrapperImpl implements NativeWrapper, + IHwBinder.DeathRecipient, getPhysicalAddressCallback { + private IHdmiCec mHdmiCec; + private final Object mLock = new Object(); + private int mPhysicalAddress = INVALID_PHYSICAL_ADDRESS; + + @Override + public String nativeInit() { + return (connectToHal() ? mHdmiCec.toString() : null); + } + + boolean connectToHal() { + try { + mHdmiCec = IHdmiCec.getService(); + try { + mHdmiCec.linkToDeath(this, HDMI_CEC_HAL_DEATH_COOKIE); + } catch (RemoteException e) { + HdmiLogger.error("Couldn't link to death : ", e); + } + } catch (RemoteException e) { + HdmiLogger.error("Couldn't get tv.cec service : ", e); + return false; + } + return true; + } + + @Override + public void setCallback(HdmiCecCallback callback) { + try { + mHdmiCec.setCallback(callback); + } catch (RemoteException e) { + HdmiLogger.error("Couldn't initialise tv.cec callback : ", e); + } + } + + @Override + public int nativeSendCecCommand(int srcAddress, int dstAddress, byte[] body) { + CecMessage message = new CecMessage(); + message.initiator = srcAddress; + message.destination = dstAddress; + message.body = new ArrayList<>(body.length); + for (byte b : body) { + message.body.add(b); + } + try { + return mHdmiCec.sendMessage(message); + } catch (RemoteException e) { + HdmiLogger.error("Failed to send CEC message : ", e); + return SendMessageResult.FAIL; + } + } + + @Override + public int nativeAddLogicalAddress(int logicalAddress) { + try { + return mHdmiCec.addLogicalAddress(logicalAddress); + } catch (RemoteException e) { + HdmiLogger.error("Failed to add a logical address : ", e); + return Result.FAILURE_INVALID_ARGS; + } + } + + @Override + public void nativeClearLogicalAddress() { + try { + mHdmiCec.clearLogicalAddress(); + } catch (RemoteException e) { + HdmiLogger.error("Failed to clear logical address : ", e); + } + } @Override - public long nativeInit(HdmiCecController handler, MessageQueue messageQueue) { - return HdmiCecController.nativeInit(handler, messageQueue); + public int nativeGetPhysicalAddress() { + try { + mHdmiCec.getPhysicalAddress(this); + return mPhysicalAddress; + } catch (RemoteException e) { + HdmiLogger.error("Failed to get physical address : ", e); + return INVALID_PHYSICAL_ADDRESS; + } } @Override - public int nativeSendCecCommand(long controllerPtr, int srcAddress, int dstAddress, - byte[] body) { - return HdmiCecController.nativeSendCecCommand(controllerPtr, srcAddress, dstAddress, body); + public int nativeGetVersion() { + try { + return mHdmiCec.getCecVersion(); + } catch (RemoteException e) { + HdmiLogger.error("Failed to get cec version : ", e); + return Result.FAILURE_UNKNOWN; + } } @Override - public int nativeAddLogicalAddress(long controllerPtr, int logicalAddress) { - return HdmiCecController.nativeAddLogicalAddress(controllerPtr, logicalAddress); + public int nativeGetVendorId() { + try { + return mHdmiCec.getVendorId(); + } catch (RemoteException e) { + HdmiLogger.error("Failed to get vendor id : ", e); + return Result.FAILURE_UNKNOWN; + } } @Override - public void nativeClearLogicalAddress(long controllerPtr) { - HdmiCecController.nativeClearLogicalAddress(controllerPtr); + public HdmiPortInfo[] nativeGetPortInfos() { + try { + ArrayList<android.hardware.tv.cec.V1_0.HdmiPortInfo> hdmiPortInfos = + mHdmiCec.getPortInfo(); + HdmiPortInfo[] hdmiPortInfo = new HdmiPortInfo[hdmiPortInfos.size()]; + int i = 0; + for (android.hardware.tv.cec.V1_0.HdmiPortInfo portInfo : hdmiPortInfos) { + hdmiPortInfo[i] = new HdmiPortInfo(portInfo.portId, + portInfo.type, + portInfo.physicalAddress, + portInfo.cecSupported, + false, + portInfo.arcSupported); + i++; + } + return hdmiPortInfo; + } catch (RemoteException e) { + HdmiLogger.error("Failed to get port information : ", e); + return null; + } } @Override - public int nativeGetPhysicalAddress(long controllerPtr) { - return HdmiCecController.nativeGetPhysicalAddress(controllerPtr); + public void nativeSetOption(int flag, boolean enabled) { + try { + mHdmiCec.setOption(flag, enabled); + } catch (RemoteException e) { + HdmiLogger.error("Failed to set option : ", e); + } } @Override - public int nativeGetVersion(long controllerPtr) { - return HdmiCecController.nativeGetVersion(controllerPtr); + public void nativeSetLanguage(String language) { + try { + mHdmiCec.setLanguage(language); + } catch (RemoteException e) { + HdmiLogger.error("Failed to set language : ", e); + } } @Override - public int nativeGetVendorId(long controllerPtr) { - return HdmiCecController.nativeGetVendorId(controllerPtr); + public void nativeEnableAudioReturnChannel(int port, boolean flag) { + try { + mHdmiCec.enableAudioReturnChannel(port, flag); + } catch (RemoteException e) { + HdmiLogger.error("Failed to enable/disable ARC : ", e); + } } @Override - public HdmiPortInfo[] nativeGetPortInfos(long controllerPtr) { - return HdmiCecController.nativeGetPortInfos(controllerPtr); + public boolean nativeIsConnected(int port) { + try { + return mHdmiCec.isConnected(port); + } catch (RemoteException e) { + HdmiLogger.error("Failed to get connection info : ", e); + return false; + } } @Override - public void nativeSetOption(long controllerPtr, int flag, boolean enabled) { - HdmiCecController.nativeSetOption(controllerPtr, flag, enabled); + public void serviceDied(long cookie) { + if (cookie == HDMI_CEC_HAL_DEATH_COOKIE) { + HdmiLogger.error(TAG, "Service died cokkie : " + cookie + "; reconnecting"); + connectToHal(); + } } @Override - public void nativeSetLanguage(long controllerPtr, String language) { - HdmiCecController.nativeSetLanguage(controllerPtr, language); + public void onValues(int result, short addr) { + if (result == Result.SUCCESS) { + synchronized (mLock) { + mPhysicalAddress = new Short(addr).intValue(); + } + } } + } + final class HdmiCecCallback extends IHdmiCecCallback.Stub { @Override - public void nativeEnableAudioReturnChannel(long controllerPtr, int port, boolean flag) { - HdmiCecController.nativeEnableAudioReturnChannel(controllerPtr, port, flag); + public void onCecMessage(CecMessage message) throws RemoteException { + byte[] body = new byte[message.body.size()]; + for (int i = 0; i < message.body.size(); i++) { + body[i] = message.body.get(i); + } + runOnServiceThread( + () -> handleIncomingCecCommand(message.initiator, message.destination, body)); } @Override - public boolean nativeIsConnected(long controllerPtr, int port) { - return HdmiCecController.nativeIsConnected(controllerPtr, port); + public void onHotplugEvent(HotplugEvent event) throws RemoteException { + runOnServiceThread(() -> handleHotplug(event.portId, event.connected)); } } diff --git a/services/core/java/com/android/server/hdmi/HdmiLogger.java b/services/core/java/com/android/server/hdmi/HdmiLogger.java index 2309293dcbd7..8da3c93de360 100644 --- a/services/core/java/com/android/server/hdmi/HdmiLogger.java +++ b/services/core/java/com/android/server/hdmi/HdmiLogger.java @@ -18,9 +18,9 @@ package com.android.server.hdmi; import android.annotation.Nullable; import android.os.SystemClock; +import android.util.Log; import android.util.Pair; import android.util.Slog; -import android.util.Log; import java.util.HashMap; @@ -71,6 +71,10 @@ final class HdmiLogger { getLogger().errorInternal(toLogString(logMessage, objs)); } + static void error(String logMessage, Exception e, Object... objs) { + getLogger().errorInternal(toLogString(logMessage + e, objs)); + } + private void errorInternal(String logMessage) { String log = updateLog(mErrorTimingCache, logMessage); if (!log.isEmpty()) { diff --git a/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java b/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java index d8acf0e331af..85544d0a1b02 100644 --- a/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java +++ b/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java @@ -678,8 +678,6 @@ public class GnssLocationProvider extends AbstractLocationProvider implements mNetworkConnectivityHandler = new GnssNetworkConnectivityHandler(context, GnssLocationProvider.this::onNetworkAvailable, mLooper, mNIHandler); - sendMessage(INITIALIZE_HANDLER, 0, null); - mGnssStatusListenerHelper = new GnssStatusListenerHelper(mContext, mHandler) { @Override protected boolean isAvailableInPlatform() { @@ -746,6 +744,8 @@ public class GnssLocationProvider extends AbstractLocationProvider implements setProperties(PROPERTIES); setAllowed(true); + + sendMessage(INITIALIZE_HANDLER, 0, null); } /** diff --git a/services/core/java/com/android/server/media/MediaRoute2Provider.java b/services/core/java/com/android/server/media/MediaRoute2Provider.java index 27216783d0d2..f882c57e49ba 100644 --- a/services/core/java/com/android/server/media/MediaRoute2Provider.java +++ b/services/core/java/com/android/server/media/MediaRoute2Provider.java @@ -62,6 +62,7 @@ abstract class MediaRoute2Provider { public abstract void setRouteVolume(long requestId, String routeId, int volume); public abstract void setSessionVolume(long requestId, String sessionId, int volume); + public abstract void prepareReleaseSession(@NonNull String sessionId); @NonNull public String getUniqueId() { diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java index d6b98e2de901..85af346aa88a 100644 --- a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java +++ b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java @@ -34,11 +34,16 @@ import android.os.IBinder.DeathRecipient; import android.os.Looper; import android.os.RemoteException; import android.os.UserHandle; +import android.text.TextUtils; import android.util.Log; import android.util.Slog; +import com.android.internal.annotations.GuardedBy; + import java.io.PrintWriter; import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; /** @@ -61,6 +66,9 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider private RouteDiscoveryPreference mLastDiscoveryPreference = null; + @GuardedBy("mLock") + final List<RoutingSessionInfo> mReleasingSessions = new ArrayList<>(); + MediaRoute2ProviderServiceProxy(@NonNull Context context, @NonNull ComponentName componentName, int userId) { super(componentName); @@ -141,6 +149,19 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider } } + @Override + public void prepareReleaseSession(@NonNull String sessionId) { + synchronized (mLock) { + for (RoutingSessionInfo session : mSessionInfos) { + if (TextUtils.equals(session.getId(), sessionId)) { + mSessionInfos.remove(session); + mReleasingSessions.add(session); + break; + } + } + } + } + public boolean hasComponentName(String packageName, String className) { return mComponentName.getPackageName().equals(packageName) && mComponentName.getClassName().equals(className); @@ -300,88 +321,97 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider } private void onSessionCreated(Connection connection, long requestId, - RoutingSessionInfo sessionInfo) { + RoutingSessionInfo newSession) { if (mActiveConnection != connection) { return; } - if (sessionInfo == null) { - Slog.w(TAG, "onSessionCreated: Ignoring null sessionInfo sent from " + mComponentName); + if (newSession == null) { + Slog.w(TAG, "onSessionCreated: Ignoring null session sent from " + mComponentName); return; } - sessionInfo = updateSessionInfo(sessionInfo); + newSession = assignProviderIdForSession(newSession); + String newSessionId = newSession.getId(); - boolean duplicateSessionAlreadyExists = false; synchronized (mLock) { - for (int i = 0; i < mSessionInfos.size(); i++) { - if (mSessionInfos.get(i).getId().equals(sessionInfo.getId())) { - duplicateSessionAlreadyExists = true; - break; - } + if (mSessionInfos.stream() + .anyMatch(session -> TextUtils.equals(session.getId(), newSessionId)) + || mReleasingSessions.stream() + .anyMatch(session -> TextUtils.equals(session.getId(), newSessionId))) { + Slog.w(TAG, "onSessionCreated: Duplicate session already exists. Ignoring."); + return; } - mSessionInfos.add(sessionInfo); + mSessionInfos.add(newSession); } - if (duplicateSessionAlreadyExists) { - Slog.w(TAG, "onSessionCreated: Duplicate session already exists. Ignoring."); - return; - } - - mCallback.onSessionCreated(this, requestId, sessionInfo); + mCallback.onSessionCreated(this, requestId, newSession); } - private void onSessionUpdated(Connection connection, RoutingSessionInfo sessionInfo) { + private void onSessionUpdated(Connection connection, RoutingSessionInfo updatedSession) { if (mActiveConnection != connection) { return; } - if (sessionInfo == null) { - Slog.w(TAG, "onSessionUpdated: Ignoring null sessionInfo sent from " + if (updatedSession == null) { + Slog.w(TAG, "onSessionUpdated: Ignoring null session sent from " + mComponentName); return; } - sessionInfo = updateSessionInfo(sessionInfo); + updatedSession = assignProviderIdForSession(updatedSession); boolean found = false; synchronized (mLock) { for (int i = 0; i < mSessionInfos.size(); i++) { - if (mSessionInfos.get(i).getId().equals(sessionInfo.getId())) { - mSessionInfos.set(i, sessionInfo); + if (mSessionInfos.get(i).getId().equals(updatedSession.getId())) { + mSessionInfos.set(i, updatedSession); found = true; break; } } - } - if (!found) { - Slog.w(TAG, "onSessionUpdated: Matching session info not found"); - return; + if (!found) { + for (RoutingSessionInfo releasingSession : mReleasingSessions) { + if (TextUtils.equals(releasingSession.getId(), updatedSession.getId())) { + return; + } + } + Slog.w(TAG, "onSessionUpdated: Matching session info not found"); + return; + } } - mCallback.onSessionUpdated(this, sessionInfo); + mCallback.onSessionUpdated(this, updatedSession); } - private void onSessionReleased(Connection connection, RoutingSessionInfo sessionInfo) { + private void onSessionReleased(Connection connection, RoutingSessionInfo releaedSession) { if (mActiveConnection != connection) { return; } - if (sessionInfo == null) { - Slog.w(TAG, "onSessionReleased: Ignoring null sessionInfo sent from " + mComponentName); + if (releaedSession == null) { + Slog.w(TAG, "onSessionReleased: Ignoring null session sent from " + mComponentName); return; } - sessionInfo = updateSessionInfo(sessionInfo); + releaedSession = assignProviderIdForSession(releaedSession); boolean found = false; synchronized (mLock) { - for (int i = 0; i < mSessionInfos.size(); i++) { - if (mSessionInfos.get(i).getId().equals(sessionInfo.getId())) { - mSessionInfos.remove(i); + for (RoutingSessionInfo session : mSessionInfos) { + if (TextUtils.equals(session.getId(), releaedSession.getId())) { + mSessionInfos.remove(session); found = true; break; } } + if (!found) { + for (RoutingSessionInfo session : mReleasingSessions) { + if (TextUtils.equals(session.getId(), releaedSession.getId())) { + mReleasingSessions.remove(session); + return; + } + } + } } if (!found) { @@ -389,10 +419,10 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider return; } - mCallback.onSessionReleased(this, sessionInfo); + mCallback.onSessionReleased(this, releaedSession); } - private RoutingSessionInfo updateSessionInfo(RoutingSessionInfo sessionInfo) { + private RoutingSessionInfo assignProviderIdForSession(RoutingSessionInfo sessionInfo) { return new RoutingSessionInfo.Builder(sessionInfo) .setOwnerPackageName(mComponentName.getPackageName()) .setProviderId(getUniqueId()) @@ -423,6 +453,7 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider mCallback.onSessionReleased(this, sessionInfo); } mSessionInfos.clear(); + mReleasingSessions.clear(); } } } diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index 72d296fc5f6b..cc9503995ad9 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -16,9 +16,7 @@ package com.android.server.media; -import static android.media.MediaRoute2ProviderService.REASON_ROUTE_NOT_AVAILABLE; import static android.media.MediaRoute2ProviderService.REASON_UNKNOWN_ERROR; -import static android.media.MediaRoute2ProviderService.REQUEST_ID_NONE; import static android.media.MediaRouter2Utils.getOriginalId; import static android.media.MediaRouter2Utils.getProviderId; @@ -33,6 +31,8 @@ import android.media.IMediaRouter2; import android.media.IMediaRouter2Manager; import android.media.MediaRoute2Info; import android.media.MediaRoute2ProviderInfo; +import android.media.MediaRoute2ProviderService; +import android.media.MediaRouter2Manager; import android.media.RouteDiscoveryPreference; import android.media.RoutingSessionInfo; import android.os.Binder; @@ -235,30 +235,17 @@ class MediaRouter2ServiceImpl { } public void requestCreateSessionWithRouter2(IMediaRouter2 router, int requestId, + long managerRequestId, RoutingSessionInfo oldSession, MediaRoute2Info route, Bundle sessionHints) { Objects.requireNonNull(router, "router must not be null"); + Objects.requireNonNull(oldSession, "oldSession must not be null"); Objects.requireNonNull(route, "route must not be null"); final long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { - requestCreateSessionWithRouter2Locked(requestId, router, route, sessionHints); - } - } finally { - Binder.restoreCallingIdentity(token); - } - } - - public void notifySessionHintsForCreatingSession(IMediaRouter2 router, - long uniqueRequestId, MediaRoute2Info route, Bundle sessionHints) { - Objects.requireNonNull(router, "router must not be null"); - Objects.requireNonNull(route, "route must not be null"); - - final long token = Binder.clearCallingIdentity(); - try { - synchronized (mLock) { - notifySessionHintsForCreatingSessionLocked(uniqueRequestId, - router, route, sessionHints); + requestCreateSessionWithRouter2Locked(requestId, managerRequestId, + router, oldSession, route, sessionHints); } } finally { Binder.restoreCallingIdentity(token); @@ -417,16 +404,14 @@ class MediaRouter2ServiceImpl { } public void requestCreateSessionWithManager(IMediaRouter2Manager manager, int requestId, - String packageName, MediaRoute2Info route) { + RoutingSessionInfo oldSession, MediaRoute2Info route) { Objects.requireNonNull(manager, "manager must not be null"); - if (TextUtils.isEmpty(packageName)) { - throw new IllegalArgumentException("packageName must not be empty"); - } + Objects.requireNonNull(oldSession, "oldSession must not be null"); final long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { - requestCreateSessionWithManagerLocked(requestId, manager, packageName, route); + requestCreateSessionWithManagerLocked(requestId, manager, oldSession, route); } } finally { Binder.restoreCallingIdentity(token); @@ -638,7 +623,8 @@ class MediaRouter2ServiceImpl { } } - private void requestCreateSessionWithRouter2Locked(int requestId, @NonNull IMediaRouter2 router, + private void requestCreateSessionWithRouter2Locked(int requestId, long managerRequestId, + @NonNull IMediaRouter2 router, @NonNull RoutingSessionInfo oldSession, @NonNull MediaRoute2Info route, @Nullable Bundle sessionHints) { final IBinder binder = router.asBinder(); final RouterRecord routerRecord = mAllRouterRecords.get(binder); @@ -647,41 +633,60 @@ class MediaRouter2ServiceImpl { return; } - if (route.isSystemRoute() && !routerRecord.mHasModifyAudioRoutingPermission - && !TextUtils.equals(route.getId(), - routerRecord.mUserRecord.mHandler.mSystemProvider.getDefaultRoute().getId())) { - Slog.w(TAG, "MODIFY_AUDIO_ROUTING permission is required to transfer to" - + route); - routerRecord.mUserRecord.mHandler.notifySessionCreationFailedToRouter( - routerRecord, requestId); - return; + if (managerRequestId != MediaRoute2ProviderService.REQUEST_ID_NONE) { + ManagerRecord manager = routerRecord.mUserRecord.mHandler.findManagerWithId( + toRequesterId(managerRequestId)); + if (manager == null || manager.mLastSessionCreationRequest == null) { + Slog.w(TAG, "requestCreateSessionWithRouter2Locked: " + + "Ignoring unknown request."); + routerRecord.mUserRecord.mHandler.notifySessionCreationFailedToRouter( + routerRecord, requestId); + return; + } + if (!TextUtils.equals(manager.mLastSessionCreationRequest.mOldSession.getId(), + oldSession.getId())) { + Slog.w(TAG, "requestCreateSessionWithRouter2Locked: " + + "Ignoring unmatched routing session."); + routerRecord.mUserRecord.mHandler.notifySessionCreationFailedToRouter( + routerRecord, requestId); + return; + } + if (!TextUtils.equals(manager.mLastSessionCreationRequest.mRoute.getId(), + route.getId())) { + // When media router has no permission + if (!routerRecord.mHasModifyAudioRoutingPermission + && manager.mLastSessionCreationRequest.mRoute.isSystemRoute() + && route.isSystemRoute()) { + route = manager.mLastSessionCreationRequest.mRoute; + } else { + Slog.w(TAG, "requestCreateSessionWithRouter2Locked: " + + "Ignoring unmatched route."); + routerRecord.mUserRecord.mHandler.notifySessionCreationFailedToRouter( + routerRecord, requestId); + return; + } + } + manager.mLastSessionCreationRequest = null; + } else { + if (route.isSystemRoute() && !routerRecord.mHasModifyAudioRoutingPermission + && !TextUtils.equals(route.getId(), + routerRecord.mUserRecord.mHandler.mSystemProvider.getDefaultRoute().getId())) { + Slog.w(TAG, "MODIFY_AUDIO_ROUTING permission is required to transfer to" + + route); + routerRecord.mUserRecord.mHandler.notifySessionCreationFailedToRouter( + routerRecord, requestId); + return; + } } long uniqueRequestId = toUniqueRequestId(routerRecord.mRouterId, requestId); routerRecord.mUserRecord.mHandler.sendMessage( obtainMessage(UserHandler::requestCreateSessionWithRouter2OnHandler, routerRecord.mUserRecord.mHandler, - uniqueRequestId, routerRecord, route, + uniqueRequestId, managerRequestId, routerRecord, oldSession, route, sessionHints)); } - private void notifySessionHintsForCreatingSessionLocked(long uniqueRequestId, - @NonNull IMediaRouter2 router, - @NonNull MediaRoute2Info route, @Nullable Bundle sessionHints) { - final IBinder binder = router.asBinder(); - final RouterRecord routerRecord = mAllRouterRecords.get(binder); - - if (routerRecord == null) { - Slog.w(TAG, "notifySessionHintsForCreatingSessionLocked: Ignoring unknown router."); - return; - } - - routerRecord.mUserRecord.mHandler.sendMessage( - obtainMessage(UserHandler::requestCreateSessionWithManagerOnHandler, - routerRecord.mUserRecord.mHandler, - uniqueRequestId, routerRecord, route, sessionHints)); - } - private void selectRouteWithRouter2Locked(@NonNull IMediaRouter2 router, @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) { final IBinder binder = router.asBinder(); @@ -853,27 +858,46 @@ class MediaRouter2ServiceImpl { private void requestCreateSessionWithManagerLocked(int requestId, @NonNull IMediaRouter2Manager manager, - @NonNull String packageName, @NonNull MediaRoute2Info route) { + @NonNull RoutingSessionInfo oldSession, @NonNull MediaRoute2Info route) { ManagerRecord managerRecord = mAllManagerRecords.get(manager.asBinder()); if (managerRecord == null) { return; } + String packageName = oldSession.getClientPackageName(); + RouterRecord routerRecord = managerRecord.mUserRecord.findRouterRecordLocked(packageName); if (routerRecord == null) { Slog.w(TAG, "requestCreateSessionWithManagerLocked: Ignoring session creation for " + "unknown router."); + try { + managerRecord.mManager.notifyRequestFailed(requestId, REASON_UNKNOWN_ERROR); + } catch (RemoteException ex) { + Slog.w(TAG, "requestCreateSessionWithManagerLocked: Failed to notify failure. " + + "Manager probably died."); + } return; } long uniqueRequestId = toUniqueRequestId(managerRecord.mManagerId, requestId); + if (managerRecord.mLastSessionCreationRequest != null) { + managerRecord.mUserRecord.mHandler.notifyRequestFailedToManager( + managerRecord.mManager, + toOriginalRequestId(managerRecord.mLastSessionCreationRequest + .mManagerRequestId), + REASON_UNKNOWN_ERROR); + managerRecord.mLastSessionCreationRequest = null; + } + managerRecord.mLastSessionCreationRequest = new SessionCreationRequest(routerRecord, + MediaRoute2ProviderService.REQUEST_ID_NONE, uniqueRequestId, + oldSession, route); // Before requesting to the provider, get session hints from the media router. // As a return, media router will request to create a session. routerRecord.mUserRecord.mHandler.sendMessage( - obtainMessage(UserHandler::getSessionHintsForCreatingSessionOnHandler, + obtainMessage(UserHandler::requestRouterCreateSessionOnHandler, routerRecord.mUserRecord.mHandler, - uniqueRequestId, routerRecord, managerRecord, route)); + uniqueRequestId, routerRecord, managerRecord, oldSession, route)); } private void selectRouteWithManagerLocked(int requestId, @NonNull IMediaRouter2Manager manager, @@ -887,7 +911,7 @@ class MediaRouter2ServiceImpl { // Can be null if the session is system's or RCN. RouterRecord routerRecord = managerRecord.mUserRecord.mHandler - .findRouterforSessionLocked(uniqueSessionId); + .findRouterWithSessionLocked(uniqueSessionId); long uniqueRequestId = toUniqueRequestId(managerRecord.mManagerId, requestId); managerRecord.mUserRecord.mHandler.sendMessage( @@ -908,7 +932,7 @@ class MediaRouter2ServiceImpl { // Can be null if the session is system's or RCN. RouterRecord routerRecord = managerRecord.mUserRecord.mHandler - .findRouterforSessionLocked(uniqueSessionId); + .findRouterWithSessionLocked(uniqueSessionId); long uniqueRequestId = toUniqueRequestId(managerRecord.mManagerId, requestId); managerRecord.mUserRecord.mHandler.sendMessage( @@ -929,7 +953,7 @@ class MediaRouter2ServiceImpl { // Can be null if the session is system's or RCN. RouterRecord routerRecord = managerRecord.mUserRecord.mHandler - .findRouterforSessionLocked(uniqueSessionId); + .findRouterWithSessionLocked(uniqueSessionId); long uniqueRequestId = toUniqueRequestId(managerRecord.mManagerId, requestId); managerRecord.mUserRecord.mHandler.sendMessage( @@ -966,7 +990,7 @@ class MediaRouter2ServiceImpl { } RouterRecord routerRecord = managerRecord.mUserRecord.mHandler - .findRouterforSessionLocked(uniqueSessionId); + .findRouterWithSessionLocked(uniqueSessionId); long uniqueRequestId = toUniqueRequestId(managerRecord.mManagerId, requestId); managerRecord.mUserRecord.mHandler.sendMessage( @@ -1097,6 +1121,7 @@ class MediaRouter2ServiceImpl { public final int mPid; public final String mPackageName; public final int mManagerId; + public SessionCreationRequest mLastSessionCreationRequest; ManagerRecord(UserRecord userRecord, IMediaRouter2Manager manager, int uid, int pid, String packageName) { @@ -1222,10 +1247,20 @@ class MediaRouter2ServiceImpl { } @Nullable - public RouterRecord findRouterforSessionLocked(@NonNull String uniqueSessionId) { + public RouterRecord findRouterWithSessionLocked(@NonNull String uniqueSessionId) { return mSessionToRouterMap.get(uniqueSessionId); } + @Nullable + public ManagerRecord findManagerWithId(int managerId) { + for (ManagerRecord manager : getManagerRecords()) { + if (manager.mManagerId == managerId) { + return manager; + } + } + return null; + } + private void onProviderStateChangedOnHandler(@NonNull MediaRoute2Provider provider) { int providerInfoIndex = getLastProviderInfoIndex(provider.getUniqueId()); MediaRoute2ProviderInfo currentInfo = provider.getProviderInfo(); @@ -1318,26 +1353,28 @@ class MediaRouter2ServiceImpl { return -1; } - private void getSessionHintsForCreatingSessionOnHandler(long uniqueRequestId, + private void requestRouterCreateSessionOnHandler(long uniqueRequestId, @NonNull RouterRecord routerRecord, @NonNull ManagerRecord managerRecord, - @NonNull MediaRoute2Info route) { - SessionCreationRequest request = - new SessionCreationRequest(routerRecord, uniqueRequestId, route, managerRecord); - mSessionCreationRequests.add(request); - + @NonNull RoutingSessionInfo oldSession, @NonNull MediaRoute2Info route) { try { - routerRecord.mRouter.getSessionHintsForCreatingSession(uniqueRequestId, route); + if (route.isSystemRoute() && !routerRecord.mHasModifyAudioRoutingPermission) { + routerRecord.mRouter.requestCreateSessionByManager(uniqueRequestId, + oldSession, mSystemProvider.getDefaultRoute()); + } else { + routerRecord.mRouter.requestCreateSessionByManager(uniqueRequestId, + oldSession, route); + } } catch (RemoteException ex) { Slog.w(TAG, "getSessionHintsForCreatingSessionOnHandler: " + "Failed to request. Router probably died.", ex); - mSessionCreationRequests.remove(request); notifyRequestFailedToManager(managerRecord.mManager, toOriginalRequestId(uniqueRequestId), REASON_UNKNOWN_ERROR); } } private void requestCreateSessionWithRouter2OnHandler(long uniqueRequestId, - @NonNull RouterRecord routerRecord, + long managerRequestId, @NonNull RouterRecord routerRecord, + @NonNull RoutingSessionInfo oldSession, @NonNull MediaRoute2Info route, @Nullable Bundle sessionHints) { final MediaRoute2Provider provider = findProvider(route.getProviderId()); @@ -1350,49 +1387,14 @@ class MediaRouter2ServiceImpl { } SessionCreationRequest request = - new SessionCreationRequest(routerRecord, uniqueRequestId, route, null); + new SessionCreationRequest(routerRecord, uniqueRequestId, + managerRequestId, oldSession, route); mSessionCreationRequests.add(request); provider.requestCreateSession(uniqueRequestId, routerRecord.mPackageName, route.getOriginalId(), sessionHints); } - private void requestCreateSessionWithManagerOnHandler(long uniqueRequestId, - @NonNull RouterRecord routerRecord, - @NonNull MediaRoute2Info route, @Nullable Bundle sessionHints) { - SessionCreationRequest matchingRequest = null; - for (SessionCreationRequest request : mSessionCreationRequests) { - if (request.mUniqueRequestId == uniqueRequestId) { - matchingRequest = request; - break; - } - } - if (matchingRequest == null) { - Slog.w(TAG, "requestCreateSessionWithManagerOnHandler: " - + "Ignoring an unknown session creation request."); - return; - } - - if (!TextUtils.equals(matchingRequest.mRoute.getId(), route.getId())) { - Slog.w(TAG, "requestCreateSessionWithManagerOnHandler: " - + "The given route is different from the requested route."); - return; - } - - final MediaRoute2Provider provider = findProvider(route.getProviderId()); - if (provider == null) { - Slog.w(TAG, "requestCreateSessionWithManagerOnHandler: Ignoring session " - + "creation request since no provider found for given route=" + route); - mSessionCreationRequests.remove(matchingRequest); - notifyRequestFailedToManager(matchingRequest.mRequestedManagerRecord.mManager, - toOriginalRequestId(uniqueRequestId), REASON_ROUTE_NOT_AVAILABLE); - return; - } - - provider.requestCreateSession(uniqueRequestId, routerRecord.mPackageName, - route.getOriginalId(), sessionHints); - } - // routerRecord can be null if the session is system's or RCN. private void selectRouteOnHandler(long uniqueRequestId, @Nullable RouterRecord routerRecord, @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) { @@ -1539,25 +1541,23 @@ class MediaRouter2ServiceImpl { private void onSessionCreatedOnHandler(@NonNull MediaRoute2Provider provider, long uniqueRequestId, @NonNull RoutingSessionInfo sessionInfo) { - notifySessionCreatedToManagers(getManagers(), - toOriginalRequestId(uniqueRequestId), sessionInfo); - - if (uniqueRequestId == REQUEST_ID_NONE) { - // The session is created without any matching request. - return; - } - SessionCreationRequest matchingRequest = null; for (SessionCreationRequest request : mSessionCreationRequests) { if (request.mUniqueRequestId == uniqueRequestId && TextUtils.equals( - request.mRoute.getProviderId(), provider.getUniqueId())) { + request.mRoute.getProviderId(), provider.getUniqueId())) { matchingRequest = request; break; } } + long managerRequestId = (matchingRequest == null) + ? MediaRoute2ProviderService.REQUEST_ID_NONE + : matchingRequest.mManagerRequestId; + // Managers should know created session even if it's not requested. + notifySessionCreatedToManagers(managerRequestId, sessionInfo); + if (matchingRequest == null) { Slog.w(TAG, "Ignoring session creation result for unknown request. " + "uniqueRequestId=" + uniqueRequestId + ", sessionInfo=" + sessionInfo); @@ -1565,12 +1565,14 @@ class MediaRouter2ServiceImpl { } mSessionCreationRequests.remove(matchingRequest); - - if (sessionInfo == null) { - // Failed - notifySessionCreationFailedToRouter(matchingRequest.mRouterRecord, - toOriginalRequestId(uniqueRequestId)); - return; + // Not to show old session + MediaRoute2Provider oldProvider = + findProvider(matchingRequest.mOldSession.getProviderId()); + if (oldProvider != null) { + oldProvider.prepareReleaseSession(matchingRequest.mOldSession.getId()); + } else { + Slog.w(TAG, "onSessionCreatedOnHandler: Can't find provider for an old session. " + + "session=" + matchingRequest.mOldSession); } String originalRouteId = matchingRequest.mRoute.getId(); @@ -1645,23 +1647,17 @@ class MediaRouter2ServiceImpl { } final int requesterId = toRequesterId(uniqueRequestId); - for (ManagerRecord manager : getManagerRecords()) { - if (manager.mManagerId == requesterId) { - notifyRequestFailedToManager( - manager.mManager, toOriginalRequestId(uniqueRequestId), reason); - return; - } + ManagerRecord manager = findManagerWithId(requesterId); + if (manager != null) { + notifyRequestFailedToManager( + manager.mManager, toOriginalRequestId(uniqueRequestId), reason); + return; } // Currently, only the manager can get notified of failures. // TODO: Notify router too when the related callback is introduced. } - // TODO(b/157873556): Find a way to prevent providers from notifying error on random reqID. - // Possible solutions can be: - // 1) Record the other type of requests too (not only session creation request) - // 2) Throw exception on providers when they try to notify error on - // random uniqueRequestId. private boolean handleSessionCreationRequestFailed(@NonNull MediaRoute2Provider provider, long uniqueRequestId, int reason) { // Check whether the failure is about creating a session @@ -1683,12 +1679,16 @@ class MediaRouter2ServiceImpl { // Notify the requester about the failure. // The call should be made by either MediaRouter2 or MediaRouter2Manager. - if (matchingRequest.mRequestedManagerRecord == null) { + if (matchingRequest.mManagerRequestId == MediaRouter2Manager.REQUEST_ID_NONE) { notifySessionCreationFailedToRouter( matchingRequest.mRouterRecord, toOriginalRequestId(uniqueRequestId)); } else { - notifyRequestFailedToManager(matchingRequest.mRequestedManagerRecord.mManager, - toOriginalRequestId(uniqueRequestId), reason); + final int requesterId = toRequesterId(matchingRequest.mManagerRequestId); + ManagerRecord manager = findManagerWithId(requesterId); + if (manager != null) { + notifyRequestFailedToManager(manager.mManager, + toOriginalRequestId(matchingRequest.mManagerRequestId), reason); + } } return true; } @@ -1921,14 +1921,19 @@ class MediaRouter2ServiceImpl { } } - private void notifySessionCreatedToManagers(@NonNull List<IMediaRouter2Manager> managers, - int requestId, @NonNull RoutingSessionInfo sessionInfo) { - for (IMediaRouter2Manager manager : managers) { + private void notifySessionCreatedToManagers(long managerRequestId, + @NonNull RoutingSessionInfo session) { + int requesterId = toRequesterId(managerRequestId); + int originalRequestId = toOriginalRequestId(managerRequestId); + + for (ManagerRecord manager : getManagerRecords()) { try { - manager.notifySessionCreated(requestId, sessionInfo); + manager.mManager.notifySessionCreated( + ((manager.mManagerId == requesterId) ? originalRequestId : + MediaRouter2Manager.REQUEST_ID_NONE), session); } catch (RemoteException ex) { Slog.w(TAG, "notifySessionCreatedToManagers: " - + "failed to notify. Manager probably died.", ex); + + "Failed to notify. Manager probably died.", ex); } } } @@ -2014,7 +2019,7 @@ class MediaRouter2ServiceImpl { } mUserRecord.mCompositeDiscoveryPreference = new RouteDiscoveryPreference.Builder(discoveryPreferences) - .build(); + .build(); } for (MediaRoute2Provider provider : mRouteProviders) { provider.updateDiscoveryPreference(mUserRecord.mCompositeDiscoveryPreference); @@ -2030,21 +2035,22 @@ class MediaRouter2ServiceImpl { return null; } - final class SessionCreationRequest { - public final RouterRecord mRouterRecord; - public final long mUniqueRequestId; - public final MediaRoute2Info mRoute; - public final ManagerRecord mRequestedManagerRecord; - - // requestedManagerRecord is not null only when the request is made by manager. - SessionCreationRequest(@NonNull RouterRecord routerRecord, long uniqueRequestId, - @NonNull MediaRoute2Info route, - @Nullable ManagerRecord requestedManagerRecord) { - mRouterRecord = routerRecord; - mUniqueRequestId = uniqueRequestId; - mRoute = route; - mRequestedManagerRecord = requestedManagerRecord; - } + } + static final class SessionCreationRequest { + public final RouterRecord mRouterRecord; + public final long mUniqueRequestId; + public final long mManagerRequestId; + public final RoutingSessionInfo mOldSession; + public final MediaRoute2Info mRoute; + + SessionCreationRequest(@NonNull RouterRecord routerRecord, long uniqueRequestId, + long managerRequestId, @NonNull RoutingSessionInfo oldSession, + @NonNull MediaRoute2Info route) { + mRouterRecord = routerRecord; + mUniqueRequestId = uniqueRequestId; + mManagerRequestId = managerRequestId; + mOldSession = oldSession; + mRoute = route; } } } diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java index 3337b480d6a8..0e52a67c8d39 100644 --- a/services/core/java/com/android/server/media/MediaRouterService.java +++ b/services/core/java/com/android/server/media/MediaRouterService.java @@ -481,16 +481,10 @@ public final class MediaRouterService extends IMediaRouterService.Stub // Binder call @Override public void requestCreateSessionWithRouter2(IMediaRouter2 router, int requestId, + long managerRequestId, RoutingSessionInfo oldSession, MediaRoute2Info route, Bundle sessionHints) { - mService2.requestCreateSessionWithRouter2(router, requestId, route, sessionHints); - } - - // Binder call - @Override - public void notifySessionHintsForCreatingSession(IMediaRouter2 router, - long uniqueRequestId, MediaRoute2Info route, Bundle sessionHints) { - mService2.notifySessionHintsForCreatingSession(router, - uniqueRequestId, route, sessionHints); + mService2.requestCreateSessionWithRouter2(router, requestId, managerRequestId, + oldSession, route, sessionHints); } // Binder call @@ -558,8 +552,8 @@ public final class MediaRouterService extends IMediaRouterService.Stub // Binder call @Override public void requestCreateSessionWithManager(IMediaRouter2Manager manager, - int requestId, String packageName, MediaRoute2Info route) { - mService2.requestCreateSessionWithManager(manager, requestId, packageName, route); + int requestId, RoutingSessionInfo oldSession, MediaRoute2Info route) { + mService2.requestCreateSessionWithManager(manager, requestId, oldSession, route); } // Binder call diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java index 242132c8e5ff..b45d450cd200 100644 --- a/services/core/java/com/android/server/media/MediaSessionService.java +++ b/services/core/java/com/android/server/media/MediaSessionService.java @@ -59,6 +59,7 @@ import android.net.Uri; import android.os.Binder; import android.os.Bundle; import android.os.Handler; +import android.os.HandlerThread; import android.os.IBinder; import android.os.Message; import android.os.PowerManager; @@ -119,6 +120,7 @@ public class MediaSessionService extends SystemService implements Monitor { private final PowerManager.WakeLock mMediaEventWakeLock; private final INotificationManager mNotificationManager; private final Object mLock = new Object(); + private final HandlerThread mRecordThread = new HandlerThread("SessionRecordThread"); // Keeps the full user id for each user. @GuardedBy("mLock") private final SparseIntArray mFullUserIds = new SparseIntArray(); @@ -198,6 +200,7 @@ public class MediaSessionService extends SystemService implements Monitor { instantiateCustomProvider(null); instantiateCustomDispatcher(null); + mRecordThread.start(); } private boolean isGlobalPriorityActiveLocked() { @@ -599,8 +602,8 @@ public class MediaSessionService extends SystemService implements Monitor { final MediaSessionRecord session; try { session = new MediaSessionRecord(callerPid, callerUid, userId, - callerPackageName, cb, tag, sessionInfo, this, mHandler.getLooper(), - policies); + callerPackageName, cb, tag, sessionInfo, this, + mRecordThread.getLooper(), policies); } catch (RemoteException e) { throw new RuntimeException("Media Session owner died prematurely.", e); } @@ -1157,8 +1160,8 @@ public class MediaSessionService extends SystemService implements Monitor { throw new SecurityException("Unexpected Session2Token's UID, expected=" + uid + " but actually=" + sessionToken.getUid()); } - MediaSession2Record record = new MediaSession2Record( - sessionToken, MediaSessionService.this, mHandler.getLooper(), 0); + MediaSession2Record record = new MediaSession2Record(sessionToken, + MediaSessionService.this, mRecordThread.getLooper(), 0); synchronized (mLock) { FullUserRecord user = getFullUserRecordLocked(record.getUserId()); user.mPriorityStack.addSession(record); diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java index 42d4c88959bd..2c089ca8300e 100644 --- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java +++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java @@ -222,6 +222,11 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { // Do nothing since we don't support grouping volume yet. } + @Override + public void prepareReleaseSession(String sessionId) { + // Do nothing since the system session persists. + } + public MediaRoute2Info getDefaultRoute() { return mDefaultRoute; } diff --git a/services/core/java/com/android/server/pm/AppsFilter.java b/services/core/java/com/android/server/pm/AppsFilter.java index 8a7702efcba8..3a203d52edbe 100644 --- a/services/core/java/com/android/server/pm/AppsFilter.java +++ b/services/core/java/com/android/server/pm/AppsFilter.java @@ -280,11 +280,15 @@ public class AppsFilter { @Override public void onCompatChange(String packageName) { - updateEnabledState(mPmInternal.getPackage(packageName)); + AndroidPackage pkg = mPmInternal.getPackage(packageName); + if (pkg == null) { + return; + } + updateEnabledState(pkg); mAppsFilter.updateShouldFilterCacheForPackage(packageName); } - private void updateEnabledState(AndroidPackage pkg) { + private void updateEnabledState(@NonNull AndroidPackage pkg) { // TODO(b/135203078): Do not use toAppInfo final boolean enabled = mInjector.getCompatibility().isChangeEnabledInternal( PackageManager.FILTER_APPLICATION_QUERY, pkg.toAppInfoWithoutState()); @@ -297,12 +301,12 @@ public class AppsFilter { @Override public void updatePackageState(PackageSetting setting, boolean removed) { - final boolean enableLogging = + final boolean enableLogging = setting.pkg != null && !removed && (setting.pkg.isTestOnly() || setting.pkg.isDebuggable()); enableLogging(setting.appId, enableLogging); if (removed) { - mDisabledPackages.remove(setting.pkg.getPackageName()); - } else { + mDisabledPackages.remove(setting.name); + } else if (setting.pkg != null) { updateEnabledState(setting.pkg); } } @@ -495,10 +499,15 @@ public class AppsFilter { * Adds a package that should be considered when filtering visibility between apps. * * @param newPkgSetting the new setting being added + * @param isReplace if the package is being replaced and may need extra cleanup. */ - public void addPackage(PackageSetting newPkgSetting) { + public void addPackage(PackageSetting newPkgSetting, boolean isReplace) { Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "filter.addPackage"); try { + if (isReplace) { + // let's first remove any prior rules for this package + removePackage(newPkgSetting); + } mStateProvider.runWithState((settings, users) -> { addPackageInternal(newPkgSetting, settings); if (mShouldFilterCache != null) { @@ -578,8 +587,9 @@ public class AppsFilter { } } // if either package instruments the other, mark both as visible to one another - if (pkgInstruments(newPkgSetting, existingSetting) - || pkgInstruments(existingSetting, newPkgSetting)) { + if (newPkgSetting.pkg != null && existingSetting.pkg != null + && (pkgInstruments(newPkgSetting.pkg, existingSetting.pkg) + || pkgInstruments(existingSetting.pkg, newPkgSetting.pkg))) { mQueriesViaPackage.add(newPkgSetting.appId, existingSetting.appId); mQueriesViaPackage.add(existingSetting.appId, newPkgSetting.appId); } @@ -777,12 +787,20 @@ public class AppsFilter { } /** + * Equivalent to calling {@link #addPackage(PackageSetting, boolean)} with {@code isReplace} + * equal to {@code false}. + * @see AppsFilter#addPackage(PackageSetting, boolean) + */ + public void addPackage(PackageSetting newPkgSetting) { + addPackage(newPkgSetting, false /* isReplace */); + } + + /** * Removes a package for consideration when filtering visibility between apps. * * @param setting the setting of the package being removed. */ public void removePackage(PackageSetting setting) { - removeAppIdFromVisibilityCache(setting.appId); mStateProvider.runWithState((settings, users) -> { final int userCount = users.length; for (int u = 0; u < userCount; u++) { @@ -805,17 +823,7 @@ public class AppsFilter { mQueriesViaPackage.remove(mQueriesViaPackage.keyAt(i), setting.appId); } - // re-add other shared user members to re-establish visibility between them and other - // packages - if (setting.sharedUser != null) { - for (int i = setting.sharedUser.packages.size() - 1; i >= 0; i--) { - if (setting.sharedUser.packages.valueAt(i) == setting) { - continue; - } - addPackageInternal( - setting.sharedUser.packages.valueAt(i), settings); - } - } + mForceQueryable.remove(setting.appId); if (setting.pkg != null && !setting.pkg.getProtectedBroadcasts().isEmpty()) { final String removingPackageName = setting.pkg.getPackageName(); @@ -829,6 +837,21 @@ public class AppsFilter { mOverlayReferenceMapper.removePkg(setting.name); mFeatureConfig.updatePackageState(setting, true /*removed*/); + // After removing all traces of the package, if it's part of a shared user, re-add other + // shared user members to re-establish visibility between them and other packages. + // NOTE: this must come after all removals from data structures but before we update the + // cache + if (setting.sharedUser != null) { + for (int i = setting.sharedUser.packages.size() - 1; i >= 0; i--) { + if (setting.sharedUser.packages.valueAt(i) == setting) { + continue; + } + addPackageInternal( + setting.sharedUser.packages.valueAt(i), settings); + } + } + + removeAppIdFromVisibilityCache(setting.appId); if (mShouldFilterCache != null && setting.sharedUser != null) { for (int i = setting.sharedUser.packages.size() - 1; i >= 0; i--) { PackageSetting siblingSetting = setting.sharedUser.packages.valueAt(i); @@ -840,9 +863,6 @@ public class AppsFilter { } } }); - mForceQueryable.remove(setting.appId); - - } /** @@ -1091,16 +1111,14 @@ public class AppsFilter { } /** Returns {@code true} if the source package instruments the target package. */ - private static boolean pkgInstruments(PackageSetting source, PackageSetting target) { + private static boolean pkgInstruments( + @NonNull AndroidPackage source, @NonNull AndroidPackage target) { try { Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "pkgInstruments"); - final String packageName = target.pkg.getPackageName(); - final List<ParsedInstrumentation> inst = source.pkg.getInstrumentations(); + final String packageName = target.getPackageName(); + final List<ParsedInstrumentation> inst = source.getInstrumentations(); for (int i = ArrayUtils.size(inst) - 1; i >= 0; i--) { if (Objects.equals(inst.get(i).getTargetPackage(), packageName)) { - if (DEBUG_LOGGING) { - log(source, target, "instrumentation"); - } return true; } } diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index de8ad6b7db13..994cec2b1e59 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -401,6 +401,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private boolean mDataLoaderFinished = false; + // TODO(b/159663586): should be protected by mLock private IncrementalFileStorages mIncrementalFileStorages; private static final FileFilter sAddedApkFilter = new FileFilter() { @@ -1353,7 +1354,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private boolean markAsSealed(@NonNull IntentSender statusReceiver, boolean forTransfer) { Objects.requireNonNull(statusReceiver); - List<PackageInstallerSession> childSessions = getChildSessions(); + List<PackageInstallerSession> childSessions = getChildSessionsNotLocked(); synchronized (mLock) { assertCallerIsOwnerOrRootLocked(); @@ -1436,7 +1437,11 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { * * <p> This method is handy to prevent potential deadlocks (b/123391593) */ - private @Nullable List<PackageInstallerSession> getChildSessions() { + private @Nullable List<PackageInstallerSession> getChildSessionsNotLocked() { + if (Thread.holdsLock(mLock)) { + Slog.wtf(TAG, "Calling thread " + Thread.currentThread().getName() + + " is holding mLock", new Throwable()); + } List<PackageInstallerSession> childSessions = null; if (isMultiPackage()) { final int[] childSessionIds = getChildSessionIds(); @@ -1605,7 +1610,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { return; } } - List<PackageInstallerSession> childSessions = getChildSessions(); + List<PackageInstallerSession> childSessions = getChildSessionsNotLocked(); synchronized (mLock) { try { sealLocked(childSessions); @@ -1649,7 +1654,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { throw new SecurityException("Can only transfer sessions that use public options"); } - List<PackageInstallerSession> childSessions = getChildSessions(); + List<PackageInstallerSession> childSessions = getChildSessionsNotLocked(); synchronized (mLock) { assertCallerIsOwnerOrRootLocked(); @@ -1701,7 +1706,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { // outside of the lock, because reading the child // sessions with the lock held could lead to deadlock // (b/123391593). - List<PackageInstallerSession> childSessions = getChildSessions(); + List<PackageInstallerSession> childSessions = getChildSessionsNotLocked(); try { synchronized (mLock) { @@ -2602,6 +2607,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { "Session " + sessionId + " is a child of multi-package session " + mParentSessionId + " and may not be abandoned directly."); } + + List<PackageInstallerSession> childSessions = getChildSessionsNotLocked(); synchronized (mLock) { if (params.isStaged && mDestroyed) { // If a user abandons staged session in an unsafe state, then system will try to @@ -2625,7 +2632,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { mCallback.onStagedSessionChanged(this); return; } - cleanStageDir(); + cleanStageDir(childSessions); } if (mRelinquished) { @@ -3055,7 +3062,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { mStagedSessionErrorMessage = errorMessage; Slog.d(TAG, "Marking session " + sessionId + " as failed: " + errorMessage); } - cleanStageDir(); + cleanStageDirNotLocked(); mCallback.onStagedSessionChanged(this); } @@ -3070,7 +3077,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { mStagedSessionErrorMessage = ""; Slog.d(TAG, "Marking session " + sessionId + " as applied"); } - cleanStageDir(); + cleanStageDirNotLocked(); mCallback.onStagedSessionChanged(this); } @@ -3128,20 +3135,37 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } } - private void cleanStageDir() { - if (isMultiPackage()) { - for (int childSessionId : getChildSessionIds()) { - mSessionProvider.getSession(childSessionId).cleanStageDir(); + /** + * <b>must not hold {@link #mLock}</b> + */ + private void cleanStageDirNotLocked() { + if (Thread.holdsLock(mLock)) { + Slog.wtf(TAG, "Calling thread " + Thread.currentThread().getName() + + " is holding mLock", new Throwable()); + } + cleanStageDir(getChildSessionsNotLocked()); + } + + private void cleanStageDir(List<PackageInstallerSession> childSessions) { + if (childSessions != null) { + for (PackageInstallerSession childSession : childSessions) { + if (childSession != null) { + childSession.cleanStageDir(); + } } } else { - if (mIncrementalFileStorages != null) { - mIncrementalFileStorages.cleanUp(); - mIncrementalFileStorages = null; - } - try { - mPm.mInstaller.rmPackageDir(stageDir.getAbsolutePath()); - } catch (InstallerException ignored) { - } + cleanStageDir(); + } + } + + private void cleanStageDir() { + if (mIncrementalFileStorages != null) { + mIncrementalFileStorages.cleanUp(); + mIncrementalFileStorages = null; + } + try { + mPm.mInstaller.rmPackageDir(stageDir.getAbsolutePath()); + } catch (InstallerException ignored) { } } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index dc7ed34fb0be..088c5daf30a4 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -11733,6 +11733,8 @@ public class PackageManagerService extends IPackageManager.Stub } } else { parsedPackage + // Non system apps cannot mark any broadcast as protected + .clearProtectedBroadcasts() // non system apps can't be flagged as core .setCoreApp(false) // clear flags not applicable to regular apps @@ -11744,7 +11746,6 @@ public class PackageManagerService extends IPackageManager.Stub } if ((scanFlags & SCAN_AS_PRIVILEGED) == 0) { parsedPackage - .clearProtectedBroadcasts() .markNotActivitiesAsNotExportedIfSingleUser(); } @@ -12368,7 +12369,9 @@ public class PackageManagerService extends IPackageManager.Stub ksms.addScannedPackageLPw(pkg); mComponentResolver.addAllComponents(pkg, chatty); - mAppsFilter.addPackage(pkgSetting); + final boolean isReplace = + reconciledPkg.prepareResult != null && reconciledPkg.prepareResult.replace; + mAppsFilter.addPackage(pkgSetting, isReplace); // Don't allow ephemeral applications to define new permissions groups. if ((scanFlags & SCAN_AS_INSTANT_APP) != 0) { diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index 0dc4d131640c..1a7490e8b327 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -2281,7 +2281,7 @@ class PackageManagerShellCommand extends ShellCommand { if (grant) { mPermissionManager.grantRuntimePermission(pkg, perm, translatedUserId); } else { - mPermissionManager.revokeRuntimePermission(pkg, perm, translatedUserId); + mPermissionManager.revokeRuntimePermission(pkg, perm, translatedUserId, null); } return 0; } diff --git a/services/core/java/com/android/server/pm/TEST_MAPPING b/services/core/java/com/android/server/pm/TEST_MAPPING index eb51cc3cd25c..4b83d70ea34e 100644 --- a/services/core/java/com/android/server/pm/TEST_MAPPING +++ b/services/core/java/com/android/server/pm/TEST_MAPPING @@ -71,7 +71,12 @@ ] }, { - "name": "PackageManagerServiceHostTests" + "name": "PackageManagerServiceHostTests", + "options": [ + { + "include-annotation": "android.platform.test.annotations.Presubmit" + } + ] } ], "postsubmit": [ @@ -87,6 +92,9 @@ "name": "CtsAppSecurityHostTestCases" }, { + "name": "PackageManagerServiceHostTests" + }, + { "name": "FrameworksServicesTests", "options": [ { diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java index 1b11e2d0860d..4f0b689996f2 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java @@ -29,6 +29,9 @@ import static android.content.pm.PackageManager.FLAG_PERMISSION_GRANTED_BY_DEFAU import static android.content.pm.PackageManager.FLAG_PERMISSION_GRANTED_BY_ROLE; import static android.content.pm.PackageManager.FLAG_PERMISSION_ONE_TIME; import static android.content.pm.PackageManager.FLAG_PERMISSION_POLICY_FIXED; +import static android.content.pm.PackageManager.FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT; +import static android.content.pm.PackageManager.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT; +import static android.content.pm.PackageManager.FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT; import static android.content.pm.PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED; import static android.content.pm.PackageManager.FLAG_PERMISSION_REVOKED_COMPAT; import static android.content.pm.PackageManager.FLAG_PERMISSION_REVOKE_WHEN_REQUESTED; @@ -327,13 +330,17 @@ public class PermissionManagerService extends IPermissionManager.Stub { mPackageManagerInt.writeSettings(true); } @Override - public void onPermissionRevoked(int uid, int userId) { + public void onPermissionRevoked(int uid, int userId, String reason) { mOnPermissionChangeListeners.onPermissionsChanged(uid); // Critical; after this call the application should never have the permission mPackageManagerInt.writeSettings(false); final int appId = UserHandle.getAppId(uid); - mHandler.post(() -> killUid(appId, userId, KILL_APP_REASON_PERMISSIONS_REVOKED)); + if (reason == null) { + mHandler.post(() -> killUid(appId, userId, KILL_APP_REASON_PERMISSIONS_REVOKED)); + } else { + mHandler.post(() -> killUid(appId, userId, reason)); + } } @Override public void onInstallPermissionRevoked() { @@ -470,7 +477,7 @@ public class PermissionManagerService extends IPermissionManager.Stub { IActivityManager am = ActivityManager.getService(); if (am != null) { try { - am.killUid(appId, userId, reason); + am.killUidForPermissionChange(appId, userId, reason); } catch (RemoteException e) { /* ignore - same process */ } @@ -754,9 +761,9 @@ public class PermissionManagerService extends IPermissionManager.Stub { flagMask &= ~PackageManager.FLAG_PERMISSION_GRANTED_BY_DEFAULT; flagValues &= ~PackageManager.FLAG_PERMISSION_GRANTED_BY_DEFAULT; flagValues &= ~PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED; - flagValues &= ~PackageManager.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT; - flagValues &= ~PackageManager.FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT; - flagValues &= ~PackageManager.FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT; + flagValues &= ~FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT; + flagValues &= ~FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT; + flagValues &= ~FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT; flagValues &= ~PackageManager.FLAG_PERMISSION_APPLY_RESTRICTION; } @@ -1112,13 +1119,13 @@ public class PermissionManagerService extends IPermissionManager.Stub { int queryFlags = 0; if ((flags & PackageManager.FLAG_PERMISSION_WHITELIST_SYSTEM) != 0) { - queryFlags |= PackageManager.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT; + queryFlags |= FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT; } if ((flags & PackageManager.FLAG_PERMISSION_WHITELIST_UPGRADE) != 0) { - queryFlags |= PackageManager.FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT; + queryFlags |= FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT; } if ((flags & PackageManager.FLAG_PERMISSION_WHITELIST_INSTALLER) != 0) { - queryFlags |= PackageManager.FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT; + queryFlags |= FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT; } ArrayList<String> whitelistedPermissions = null; @@ -1280,8 +1287,8 @@ public class PermissionManagerService extends IPermissionManager.Stub { final long identity = Binder.clearCallingIdentity(); try { - setWhitelistedRestrictedPermissionsForUser( - pkg, userId, permissions, Process.myUid(), flags, mDefaultPermissionCallback); + setWhitelistedRestrictedPermissionsForUsers(pkg, new int[]{ userId }, permissions, + Process.myUid(), flags, mDefaultPermissionCallback); } finally { Binder.restoreCallingIdentity(identity); } @@ -1526,19 +1533,21 @@ public class PermissionManagerService extends IPermissionManager.Stub { } @Override - public void revokeRuntimePermission(String packageName, String permName, int userId) { + public void revokeRuntimePermission(String packageName, String permName, int userId, + String reason) { final int callingUid = Binder.getCallingUid(); final boolean overridePolicy = checkUidPermission(ADJUST_RUNTIME_PERMISSIONS_POLICY, callingUid) == PackageManager.PERMISSION_GRANTED; revokeRuntimePermissionInternal(permName, packageName, overridePolicy, callingUid, userId, - mDefaultPermissionCallback); + reason, mDefaultPermissionCallback); } // TODO swap permission name and package name private void revokeRuntimePermissionInternal(String permName, String packageName, - boolean overridePolicy, int callingUid, final int userId, PermissionCallback callback) { + boolean overridePolicy, int callingUid, final int userId, String reason, + PermissionCallback callback) { if (ApplicationPackageManager.DEBUG_TRACE_PERMISSION_UPDATES && ApplicationPackageManager.shouldTraceGrant(packageName, permName, userId)) { Log.i(TAG, "System is revoking " + packageName + " " @@ -1629,7 +1638,7 @@ public class PermissionManagerService extends IPermissionManager.Stub { if (callback != null) { callback.onPermissionRevoked(UserHandle.getUid(userId, - UserHandle.getAppId(pkg.getUid())), userId); + UserHandle.getAppId(pkg.getUid())), userId, reason); } if (bp.isRuntime()) { @@ -1703,7 +1712,7 @@ public class PermissionManagerService extends IPermissionManager.Stub { mDefaultPermissionCallback.onInstallPermissionGranted(); } - public void onPermissionRevoked(int uid, int userId) { + public void onPermissionRevoked(int uid, int userId, String reason) { revokedPermissions.add(IntPair.of(uid, userId)); syncUpdatedUsers.add(userId); @@ -1816,7 +1825,7 @@ public class PermissionManagerService extends IPermissionManager.Stub { } else if ((flags & FLAG_PERMISSION_REVIEW_REQUIRED) == 0) { // Otherwise, reset the permission. revokeRuntimePermissionInternal(permName, packageName, false, Process.SYSTEM_UID, - userId, delayingPermCallback); + userId, null, delayingPermCallback); } } @@ -2297,7 +2306,7 @@ public class PermissionManagerService extends IPermissionManager.Stub { try { revokeRuntimePermissionInternal(permissionName, packageName, - false, callingUid, userId, permissionCallback); + false, callingUid, userId, null, permissionCallback); } catch (IllegalArgumentException e) { Slog.e(TAG, "Could not revoke " + permissionName + " from " + packageName, e); @@ -2517,8 +2526,8 @@ public class PermissionManagerService extends IPermissionManager.Stub { if (permission.isHardOrSoftRestricted() || permission.isImmutablyRestricted()) { permissionsState.updatePermissionFlags(permission, userId, - PackageManager.FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT, - PackageManager.FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT); + FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT, + FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT); } if (targetSdkVersion < Build.VERSION_CODES.M) { permissionsState.updatePermissionFlags(permission, userId, @@ -3756,8 +3765,8 @@ public class PermissionManagerService extends IPermissionManager.Stub { } } - private void setWhitelistedRestrictedPermissionsForUser(@NonNull AndroidPackage pkg, - @UserIdInt int userId, @Nullable List<String> permissions, int callingUid, + private void setWhitelistedRestrictedPermissionsForUsers(@NonNull AndroidPackage pkg, + @UserIdInt int[] userIds, @Nullable List<String> permissions, int callingUid, @PermissionWhitelistFlags int whitelistFlags, PermissionCallback callback) { final PermissionsState permissionsState = PackageManagerServiceUtils.getPermissionsState(mPackageManagerInt, pkg); @@ -3765,95 +3774,102 @@ public class PermissionManagerService extends IPermissionManager.Stub { return; } - ArraySet<String> oldGrantedRestrictedPermissions = null; + SparseArray<ArraySet<String>> oldGrantedRestrictedPermissions = new SparseArray<>(); boolean updatePermissions = false; - final int permissionCount = pkg.getRequestedPermissions().size(); - for (int i = 0; i < permissionCount; i++) { - final String permissionName = pkg.getRequestedPermissions().get(i); - final BasePermission bp = mSettings.getPermissionLocked(permissionName); + for (int i = 0; i < userIds.length; i++) { + int userId = userIds[i]; + for (int j = 0; j < permissionCount; j++) { + final String permissionName = pkg.getRequestedPermissions().get(j); - if (bp == null || !bp.isHardOrSoftRestricted()) { - continue; - } + final BasePermission bp = mSettings.getPermissionLocked(permissionName); - if (permissionsState.hasPermission(permissionName, userId)) { - if (oldGrantedRestrictedPermissions == null) { - oldGrantedRestrictedPermissions = new ArraySet<>(); + if (bp == null || !bp.isHardOrSoftRestricted()) { + continue; } - oldGrantedRestrictedPermissions.add(permissionName); - } - - final int oldFlags = permissionsState.getPermissionFlags(permissionName, userId); - - int newFlags = oldFlags; - int mask = 0; - int whitelistFlagsCopy = whitelistFlags; - while (whitelistFlagsCopy != 0) { - final int flag = 1 << Integer.numberOfTrailingZeros(whitelistFlagsCopy); - whitelistFlagsCopy &= ~flag; - switch (flag) { - case FLAG_PERMISSION_WHITELIST_SYSTEM: { - mask |= PackageManager.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT; - if (permissions != null && permissions.contains(permissionName)) { - newFlags |= PackageManager.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT; - } else { - newFlags &= ~PackageManager.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT; + + if (permissionsState.hasPermission(permissionName, userId)) { + if (oldGrantedRestrictedPermissions.get(userId) == null) { + oldGrantedRestrictedPermissions.put(userId, new ArraySet<>()); + } + oldGrantedRestrictedPermissions.get(userId).add(permissionName); + } + + final int oldFlags = permissionsState.getPermissionFlags(permissionName, userId); + + int newFlags = oldFlags; + int mask = 0; + int whitelistFlagsCopy = whitelistFlags; + while (whitelistFlagsCopy != 0) { + final int flag = 1 << Integer.numberOfTrailingZeros(whitelistFlagsCopy); + whitelistFlagsCopy &= ~flag; + switch (flag) { + case FLAG_PERMISSION_WHITELIST_SYSTEM: { + mask |= FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT; + if (permissions != null && permissions.contains(permissionName)) { + newFlags |= FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT; + } else { + newFlags &= ~FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT; + } } - } break; - case FLAG_PERMISSION_WHITELIST_UPGRADE: { - mask |= PackageManager.FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT; - if (permissions != null && permissions.contains(permissionName)) { - newFlags |= PackageManager.FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT; - } else { - newFlags &= ~PackageManager.FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT; + break; + case FLAG_PERMISSION_WHITELIST_UPGRADE: { + mask |= FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT; + if (permissions != null && permissions.contains(permissionName)) { + newFlags |= FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT; + } else { + newFlags &= ~FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT; + } } - } break; - case FLAG_PERMISSION_WHITELIST_INSTALLER: { - mask |= PackageManager.FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT; - if (permissions != null && permissions.contains(permissionName)) { - newFlags |= PackageManager.FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT; - } else { - newFlags &= ~PackageManager.FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT; + break; + case FLAG_PERMISSION_WHITELIST_INSTALLER: { + mask |= FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT; + if (permissions != null && permissions.contains(permissionName)) { + newFlags |= FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT; + } else { + newFlags &= ~FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT; + } } - } break; + break; + } } - } - if (oldFlags == newFlags) { - continue; - } - - updatePermissions = true; + if (oldFlags == newFlags) { + continue; + } - final boolean wasWhitelisted = (oldFlags - & (PackageManager.FLAGS_PERMISSION_RESTRICTION_ANY_EXEMPT)) != 0; - final boolean isWhitelisted = (newFlags - & (PackageManager.FLAGS_PERMISSION_RESTRICTION_ANY_EXEMPT)) != 0; + updatePermissions = true; + + final boolean wasWhitelisted = (oldFlags + & (PackageManager.FLAGS_PERMISSION_RESTRICTION_ANY_EXEMPT)) != 0; + final boolean isWhitelisted = (newFlags + & (PackageManager.FLAGS_PERMISSION_RESTRICTION_ANY_EXEMPT)) != 0; + + // If the permission is policy fixed as granted but it is no longer + // on any of the whitelists we need to clear the policy fixed flag + // as whitelisting trumps policy i.e. policy cannot grant a non + // grantable permission. + if ((oldFlags & PackageManager.FLAG_PERMISSION_POLICY_FIXED) != 0) { + final boolean isGranted = permissionsState.hasPermission(permissionName, + userId); + if (!isWhitelisted && isGranted) { + mask |= PackageManager.FLAG_PERMISSION_POLICY_FIXED; + newFlags &= ~PackageManager.FLAG_PERMISSION_POLICY_FIXED; + } + } - // If the permission is policy fixed as granted but it is no longer - // on any of the whitelists we need to clear the policy fixed flag - // as whitelisting trumps policy i.e. policy cannot grant a non - // grantable permission. - if ((oldFlags & PackageManager.FLAG_PERMISSION_POLICY_FIXED) != 0) { - final boolean isGranted = permissionsState.hasPermission(permissionName, userId); - if (!isWhitelisted && isGranted) { - mask |= PackageManager.FLAG_PERMISSION_POLICY_FIXED; - newFlags &= ~PackageManager.FLAG_PERMISSION_POLICY_FIXED; + // If we are whitelisting an app that does not support runtime permissions + // we need to make sure it goes through the permission review UI at launch. + if (pkg.getTargetSdkVersion() < Build.VERSION_CODES.M + && !wasWhitelisted && isWhitelisted) { + mask |= PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED; + newFlags |= PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED; } - } - // If we are whitelisting an app that does not support runtime permissions - // we need to make sure it goes through the permission review UI at launch. - if (pkg.getTargetSdkVersion() < Build.VERSION_CODES.M - && !wasWhitelisted && isWhitelisted) { - mask |= PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED; - newFlags |= PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED; + updatePermissionFlagsInternal(permissionName, pkg.getPackageName(), mask, newFlags, + callingUid, userId, false, null /*callback*/); } - - updatePermissionFlagsInternal(permissionName, pkg.getPackageName(), mask, newFlags, - callingUid, userId, false, null /*callback*/); } if (updatePermissions) { @@ -3861,15 +3877,22 @@ public class PermissionManagerService extends IPermissionManager.Stub { restorePermissionState(pkg, false, pkg.getPackageName(), callback); // If this resulted in losing a permission we need to kill the app. - if (oldGrantedRestrictedPermissions != null) { - final int oldGrantedCount = oldGrantedRestrictedPermissions.size(); - for (int i = 0; i < oldGrantedCount; i++) { - final String permission = oldGrantedRestrictedPermissions.valueAt(i); + for (int i = 0; i < userIds.length; i++) { + int userId = userIds[i]; + ArraySet<String> oldPermsForUser = oldGrantedRestrictedPermissions.get(userId); + if (oldPermsForUser == null) { + continue; + } + + final int oldGrantedCount = oldPermsForUser.size(); + for (int j = 0; j < oldGrantedCount; j++) { + final String permission = oldPermsForUser.valueAt(j); // Sometimes we create a new permission state instance during update. final PermissionsState newPermissionsState = - PackageManagerServiceUtils.getPermissionsState(mPackageManagerInt, pkg); + PackageManagerServiceUtils.getPermissionsState(mPackageManagerInt, + pkg); if (!newPermissionsState.hasPermission(permission, userId)) { - callback.onPermissionRevoked(pkg.getUid(), userId); + callback.onPermissionRevoked(pkg.getUid(), userId, null); break; } } @@ -4228,7 +4251,7 @@ public class PermissionManagerService extends IPermissionManager.Stub { overridePolicy, Process.SYSTEM_UID, userId, - callback); + null, callback); } catch (IllegalArgumentException e) { Slog.e(TAG, "Failed to revoke " @@ -4624,10 +4647,8 @@ public class PermissionManagerService extends IPermissionManager.Stub { public void setWhitelistedRestrictedPermissions(@NonNull AndroidPackage pkg, @NonNull int[] userIds, @Nullable List<String> permissions, int callingUid, @PackageManager.PermissionWhitelistFlags int flags) { - for (int userId : userIds) { - setWhitelistedRestrictedPermissionsForUser(pkg, userId, permissions, - callingUid, flags, mDefaultPermissionCallback); - } + setWhitelistedRestrictedPermissionsForUsers(pkg, userIds, permissions, + callingUid, flags, mDefaultPermissionCallback); } @Override public void setWhitelistedRestrictedPermissions(String packageName, diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java index 4412162a5cc8..2e83b23f57d8 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java @@ -156,7 +156,7 @@ public abstract class PermissionManagerServiceInternal extends PermissionManager } public void onInstallPermissionGranted() { } - public void onPermissionRevoked(int uid, @UserIdInt int userId) { + public void onPermissionRevoked(int uid, @UserIdInt int userId, String reason) { } public void onInstallPermissionRevoked() { } diff --git a/services/core/java/com/android/server/policy/PermissionPolicyService.java b/services/core/java/com/android/server/policy/PermissionPolicyService.java index 3ee5f5097d59..37f088b170eb 100644 --- a/services/core/java/com/android/server/policy/PermissionPolicyService.java +++ b/services/core/java/com/android/server/policy/PermissionPolicyService.java @@ -825,7 +825,15 @@ public final class PermissionPolicyService extends SystemService { return; } - if (pkgInfo == null || pkg == null || pkgInfo.requestedPermissions == null) { + if (pkgInfo == null || pkg == null || pkgInfo.applicationInfo == null + || pkgInfo.requestedPermissions == null) { + return; + } + + final int uid = pkgInfo.applicationInfo.uid; + if (uid == Process.ROOT_UID || uid == Process.SYSTEM_UID) { + // Root and system server always pass permission checks, so don't touch their app + // ops to keep compatibility. return; } diff --git a/services/core/java/com/android/server/soundtrigger_middleware/README.md b/services/core/java/com/android/server/soundtrigger_middleware/README.md new file mode 100644 index 000000000000..416548d9bc5e --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/README.md @@ -0,0 +1,19 @@ +# Sound Trigger Middleware +TODO: Add component description. + +## Notes about thread synchronization +This component has some tricky thread synchronization considerations due to its layered design and +due to the fact that it is involved in both in-bound and out-bound calls from / to +external components. To avoid potential deadlocks, a strict locking order must be ensured whenever +nesting locks. The order is: +- `SoundTriggerMiddlewareValidation` lock. +- Audio policy service lock. This one is external - it should be assumed to be held whenever we're + inside the `ExternalCaptureStateTracker.setCaptureState()` call stack *AND* to be acquired from + within our calls into `AudioSessionProvider.acquireSession()`. +- `SoundTriggerModule` lock. + +This dictates careful consideration of callbacks going from `SoundTriggerModule` to +`SoundTriggerMiddlewareValidation` and especially those coming from the `setCaptureState()` path. +We always invoke those calls outside of the `SoundTriggerModule` lock, so we can lock +`SoundTriggerMiddlewareValidation`. However, in the `setCaptureState()` case, we have to use atomics +in `SoundTriggerMiddlewareValidation` and avoid the lock. diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareValidation.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareValidation.java index f4c77a0b88ca..5d25d2cb554d 100644 --- a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareValidation.java +++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareValidation.java @@ -47,6 +47,9 @@ import java.util.HashSet; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; /** @@ -328,7 +331,7 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware } /** Activity state. */ - Activity activityState = Activity.LOADED; + private AtomicInteger mActivityState = new AtomicInteger(Activity.LOADED.ordinal()); /** Human-readable description of the model. */ final String description; @@ -383,6 +386,14 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware void updateParameterSupport(int modelParam, @Nullable ModelParameterRange range) { parameterSupport.put(modelParam, range); } + + Activity getActivityState() { + return Activity.values()[mActivityState.get()]; + } + + void setActivityState(Activity activity) { + mActivityState.set(activity.ordinal()); + } } /** @@ -393,7 +404,13 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware IBinder.DeathRecipient { private final ISoundTriggerCallback mCallback; private ISoundTriggerModule mDelegate; - private @NonNull Map<Integer, ModelState> mLoadedModels = new HashMap<>(); + // While generally all the fields of this class must be changed under a lock, an exception + // is made for the specific case of changing a model state from ACTIVE to LOADED, which + // may happen as result of a recognition callback. This would happen atomically and is + // necessary in order to avoid deadlocks associated with locking from within callbacks + // possibly originating from the audio server. + private @NonNull + ConcurrentMap<Integer, ModelState> mLoadedModels = new ConcurrentHashMap<>(); private final int mHandle; private ModuleStatus mState = ModuleStatus.ALIVE; @@ -476,10 +493,9 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware if (modelState == null) { throw new IllegalStateException("Invalid handle: " + modelHandle); } - if (modelState.activityState - != ModelState.Activity.LOADED) { + if (modelState.getActivityState() != ModelState.Activity.LOADED) { throw new IllegalStateException("Model with handle: " + modelHandle - + " has invalid state for unloading: " + modelState.activityState); + + " has invalid state for unloading: " + modelState.getActivityState()); } // From here on, every exception isn't client's fault. @@ -509,19 +525,21 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware if (modelState == null) { throw new IllegalStateException("Invalid handle: " + modelHandle); } - if (modelState.activityState - != ModelState.Activity.LOADED) { + if (modelState.getActivityState() != ModelState.Activity.LOADED) { throw new IllegalStateException("Model with handle: " + modelHandle + " has invalid state for starting recognition: " - + modelState.activityState); + + modelState.getActivityState()); } // From here on, every exception isn't client's fault. try { + // Normally, we would set the state after the operation succeeds. However, since + // the activity state may be reset outside of the lock, we set it here first, + // and reset it in case of exception. + modelState.setActivityState(ModelState.Activity.ACTIVE); mDelegate.startRecognition(modelHandle, config); - modelState.activityState = - ModelState.Activity.ACTIVE; } catch (Exception e) { + modelState.setActivityState(ModelState.Activity.LOADED); throw handleException(e); } } @@ -548,8 +566,7 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware // From here on, every exception isn't client's fault. try { mDelegate.stopRecognition(modelHandle); - modelState.activityState = - ModelState.Activity.LOADED; + modelState.setActivityState(ModelState.Activity.LOADED); } catch (Exception e) { throw handleException(e); } @@ -719,7 +736,7 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware for (Map.Entry<Integer, ModelState> entry : mLoadedModels.entrySet()) { pw.print(entry.getKey()); pw.print('\t'); - pw.print(entry.getValue().activityState.name()); + pw.print(entry.getValue().getActivityState().name()); pw.print('\t'); pw.print(entry.getValue().description); pw.println(); @@ -735,48 +752,61 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware @Override public void onRecognition(int modelHandle, @NonNull RecognitionEvent event) { - synchronized (SoundTriggerMiddlewareValidation.this) { - if (event.status != RecognitionStatus.FORCED) { - mLoadedModels.get(modelHandle).activityState = - ModelState.Activity.LOADED; - } - try { - mCallback.onRecognition(modelHandle, event); - } catch (RemoteException e) { - // Dead client will be handled by binderDied() - no need to handle here. - // In any case, client callbacks are considered best effort. - Log.e(TAG, "Client callback exception.", e); + // We cannot obtain a lock on SoundTriggerMiddlewareValidation.this, since this call + // might be coming from the audio server (via setCaptureState()) while it is holding + // a lock that is also acquired while loading / unloading models. Thus, we require a + // strict locking order here, where obtaining our lock must always come first. + // To avoid this problem, we use an atomic model activity state. There is a risk of the + // model not being in the mLoadedModels map here, since it might have been stopped / + // unloaded while the event was in flight. + if (event.status != RecognitionStatus.FORCED) { + ModelState modelState = mLoadedModels.get(modelHandle); + if (modelState != null) { + modelState.setActivityState(ModelState.Activity.LOADED); } } + try { + mCallback.onRecognition(modelHandle, event); + } catch (RemoteException e) { + // Dead client will be handled by binderDied() - no need to handle here. + // In any case, client callbacks are considered best effort. + Log.e(TAG, "Client callback exception.", e); + } } @Override public void onPhraseRecognition(int modelHandle, @NonNull PhraseRecognitionEvent event) { - synchronized (SoundTriggerMiddlewareValidation.this) { - if (event.common.status != RecognitionStatus.FORCED) { - mLoadedModels.get(modelHandle).activityState = - ModelState.Activity.LOADED; - } - try { - mCallback.onPhraseRecognition(modelHandle, event); - } catch (RemoteException e) { - // Dead client will be handled by binderDied() - no need to handle here. - // In any case, client callbacks are considered best effort. - Log.e(TAG, "Client callback exception.", e); + // We cannot obtain a lock on SoundTriggerMiddlewareValidation.this, since this call + // might be coming from the audio server (via setCaptureState()) while it is holding + // a lock that is also acquired while loading / unloading models. Thus, we require a + // strict locking order here, where obtaining our lock must always come first. + // To avoid this problem, we use an atomic model activity state. There is a risk of the + // model not being in the mLoadedModels map here, since it might have been stopped / + // unloaded while the event was in flight. + if (event.common.status != RecognitionStatus.FORCED) { + ModelState modelState = mLoadedModels.get(modelHandle); + if (modelState != null) { + modelState.setActivityState(ModelState.Activity.LOADED); } } + try { + mCallback.onPhraseRecognition(modelHandle, event); + } catch (RemoteException e) { + // Dead client will be handled by binderDied() - no need to handle here. + // In any case, client callbacks are considered best effort. + Log.e(TAG, "Client callback exception.", e); + } } @Override public void onRecognitionAvailabilityChange(boolean available) { - synchronized (SoundTriggerMiddlewareValidation.this) { - try { - mCallback.onRecognitionAvailabilityChange(available); - } catch (RemoteException e) { - // Dead client will be handled by binderDied() - no need to handle here. - // In any case, client callbacks are considered best effort. - Log.e(TAG, "Client callback exception.", e); - } + // Not locking to avoid deadlocks (not affecting any state). + try { + mCallback.onRecognitionAvailabilityChange(available); + } catch (RemoteException e) { + // Dead client will be handled by binderDied() - no need to handle here. + // In any case, client callbacks are considered best effort. + Log.e(TAG, "Client callback exception.", e); } } @@ -804,10 +834,9 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware // Gracefully stop all active recognitions and unload the models. for (Map.Entry<Integer, ModelState> entry : mLoadedModels.entrySet()) { - if (entry.getValue().activityState - == ModelState.Activity.ACTIVE) { - mDelegate.stopRecognition(entry.getKey()); - } + // Idempotent call, no harm in calling even for models that are already + // stopped. + mDelegate.stopRecognition(entry.getKey()); mDelegate.unloadModel(entry.getKey()); } // Detach. diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java index 522e5e189232..f809ed4a4a2b 100644 --- a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java +++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java @@ -42,6 +42,7 @@ import android.util.Log; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; @@ -153,19 +154,29 @@ class SoundTriggerModule implements IHwBinder.DeathRecipient { * * @param active true iff external capture is active. */ - synchronized void setExternalCaptureState(boolean active) { - if (mProperties.concurrentCapture) { - // If we support concurrent capture, we don't care about any of this. - return; - } - mRecognitionAvailable = !active; - if (!mRecognitionAvailable) { - // Our module does not support recognition while a capture is active - - // need to abort all active recognitions. - for (Session session : mActiveSessions) { - session.abortActiveRecognitions(); + void setExternalCaptureState(boolean active) { + // We should never invoke callbacks while holding the lock, since this may deadlock with + // forward calls. Thus, we first gather all the callbacks we need to invoke while holding + // the lock, but invoke them after releasing it. + List<Runnable> callbacks = new LinkedList<>(); + + synchronized (this) { + if (mProperties.concurrentCapture) { + // If we support concurrent capture, we don't care about any of this. + return; + } + mRecognitionAvailable = !active; + if (!mRecognitionAvailable) { + // Our module does not support recognition while a capture is active - + // need to abort all active recognitions. + for (Session session : mActiveSessions) { + session.abortActiveRecognitions(callbacks); + } } } + for (Runnable callback : callbacks) { + callback.run(); + } for (Session session : mActiveSessions) { session.notifyRecognitionAvailability(); } @@ -329,9 +340,18 @@ class SoundTriggerModule implements IHwBinder.DeathRecipient { @Override public void startRecognition(int modelHandle, @NonNull RecognitionConfig config) { + // We should never invoke callbacks while holding the lock, since this may deadlock with + // forward calls. Thus, we first gather all the callbacks we need to invoke while holding + // the lock, but invoke them after releasing it. + List<Runnable> callbacks = new LinkedList<>(); + synchronized (SoundTriggerModule.this) { checkValid(); - mLoadedModels.get(modelHandle).startRecognition(config); + mLoadedModels.get(modelHandle).startRecognition(config, callbacks); + } + + for (Runnable callback : callbacks) { + callback.run(); } } @@ -377,10 +397,12 @@ class SoundTriggerModule implements IHwBinder.DeathRecipient { /** * Abort all currently active recognitions. + * @param callbacks Will be appended with a list of callbacks that need to be invoked + * after this method returns, without holding the module lock. */ - private void abortActiveRecognitions() { + private void abortActiveRecognitions(@NonNull List<Runnable> callbacks) { for (Model model : mLoadedModels.values()) { - model.abortActiveRecognition(); + model.abortActiveRecognition(callbacks); } } @@ -475,10 +497,11 @@ class SoundTriggerModule implements IHwBinder.DeathRecipient { return mSession.mSessionHandle; } - private void startRecognition(@NonNull RecognitionConfig config) { + private void startRecognition(@NonNull RecognitionConfig config, + @NonNull List<Runnable> callbacks) { if (!mRecognitionAvailable) { // Recognition is unavailable - send an abort event immediately. - notifyAbort(); + callbacks.add(this::notifyAbort); return; } android.hardware.soundtrigger.V2_3.RecognitionConfig hidlConfig = @@ -525,8 +548,12 @@ class SoundTriggerModule implements IHwBinder.DeathRecipient { ConversionUtil.aidl2hidlModelParameter(modelParam))); } - /** Abort the recognition, if active. */ - private void abortActiveRecognition() { + /** + * Abort the recognition, if active. + * @param callbacks Will be appended with a list of callbacks that need to be invoked + * after this method returns, without holding the module lock. + */ + private void abortActiveRecognition(List<Runnable> callbacks) { // If we're inactive, do nothing. if (getState() != ModelState.ACTIVE) { return; @@ -535,7 +562,7 @@ class SoundTriggerModule implements IHwBinder.DeathRecipient { stopRecognition(); // Notify the client that recognition has been aborted. - notifyAbort(); + callbacks.add(this::notifyAbort); } /** Notify the client that recognition has been aborted. */ @@ -577,42 +604,44 @@ class SoundTriggerModule implements IHwBinder.DeathRecipient { public void recognitionCallback( @NonNull ISoundTriggerHwCallback.RecognitionEvent recognitionEvent, int cookie) { + RecognitionEvent aidlEvent = + ConversionUtil.hidl2aidlRecognitionEvent(recognitionEvent); + aidlEvent.captureSession = mSession.mSessionHandle; synchronized (SoundTriggerModule.this) { - RecognitionEvent aidlEvent = - ConversionUtil.hidl2aidlRecognitionEvent(recognitionEvent); - aidlEvent.captureSession = mSession.mSessionHandle; - try { - mCallback.onRecognition(mHandle, aidlEvent); - } catch (RemoteException e) { - // Dead client will be handled by binderDied() - no need to handle here. - // In any case, client callbacks are considered best effort. - Log.e(TAG, "Client callback execption.", e); - } if (aidlEvent.status != RecognitionStatus.FORCED) { setState(ModelState.LOADED); } } + // The callback must be invoked outside of the lock. + try { + mCallback.onRecognition(mHandle, aidlEvent); + } catch (RemoteException e) { + // We're not expecting any exceptions here. + throw e.rethrowAsRuntimeException(); + } } @Override public void phraseRecognitionCallback( @NonNull ISoundTriggerHwCallback.PhraseRecognitionEvent phraseRecognitionEvent, int cookie) { + PhraseRecognitionEvent aidlEvent = + ConversionUtil.hidl2aidlPhraseRecognitionEvent(phraseRecognitionEvent); + aidlEvent.common.captureSession = mSession.mSessionHandle; + synchronized (SoundTriggerModule.this) { - PhraseRecognitionEvent aidlEvent = - ConversionUtil.hidl2aidlPhraseRecognitionEvent(phraseRecognitionEvent); - aidlEvent.common.captureSession = mSession.mSessionHandle; - try { - mCallback.onPhraseRecognition(mHandle, aidlEvent); - } catch (RemoteException e) { - // Dead client will be handled by binderDied() - no need to handle here. - // In any case, client callbacks are considered best effort. - Log.e(TAG, "Client callback execption.", e); - } if (aidlEvent.common.status != RecognitionStatus.FORCED) { setState(ModelState.LOADED); } } + + // The callback must be invoked outside of the lock. + try { + mCallback.onPhraseRecognition(mHandle, aidlEvent); + } catch (RemoteException e) { + // We're not expecting any exceptions here. + throw e.rethrowAsRuntimeException(); + } } } } diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java index 09fd33d5b4ed..dbdef2368c7c 100644 --- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java +++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java @@ -2575,11 +2575,17 @@ public class StatsPullAtomService extends SystemService { lastHighWaterMark, section, true, statsFiles, procStats); procStats.dumpAggregatedProtoForStatsd(protoStreams, MAX_PROCSTATS_RAW_SHARD_SIZE); - for (ProtoOutputStream proto : protoStreams) { - if (proto.getBytes().length > 0) { + for (int i = 0; i < protoStreams.length; i++) { + byte[] bytes = protoStreams[i].getBytes(); // cache the value + if (bytes.length > 0) { StatsEvent e = StatsEvent.newBuilder() .setAtomId(atomTag) - .writeByteArray(proto.getBytes()) + .writeByteArray(bytes) + // This is a shard ID, and is specified in the metric definition to be + // a dimension. This will result in statsd using RANDOM_ONE_SAMPLE to + // keep all the shards, as it thinks each shard is a different dimension + // of data. + .writeInt(i) .build(); pulledData.add(e); } diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index 4ba58bd259fc..39e839dfc7e4 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -52,6 +52,7 @@ import android.view.WindowInsetsController.Appearance; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; +import com.android.internal.inputmethod.SoftInputShowHideReason; import com.android.internal.statusbar.IStatusBar; import com.android.internal.statusbar.IStatusBarService; import com.android.internal.statusbar.NotificationVisibility; @@ -61,6 +62,7 @@ import com.android.internal.util.DumpUtils; import com.android.internal.view.AppearanceRegion; import com.android.server.LocalServices; import com.android.server.UiThread; +import com.android.server.inputmethod.InputMethodManagerInternal; import com.android.server.notification.NotificationDelegate; import com.android.server.policy.GlobalActionsProvider; import com.android.server.power.ShutdownThread; @@ -1402,6 +1404,17 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } @Override + public void hideCurrentInputMethodForBubbles() { + final long token = Binder.clearCallingIdentity(); + try { + InputMethodManagerInternal.get().hideCurrentInputMethod( + SoftInputShowHideReason.HIDE_BUBBLES); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override public void grantInlineReplyUriPermission(String key, Uri uri, UserHandle user, String packageName) { enforceStatusBarService(); diff --git a/services/core/java/com/android/server/uri/UriGrantsManagerService.java b/services/core/java/com/android/server/uri/UriGrantsManagerService.java index c38d649ada9b..5f6323369d0a 100644 --- a/services/core/java/com/android/server/uri/UriGrantsManagerService.java +++ b/services/core/java/com/android/server/uri/UriGrantsManagerService.java @@ -115,7 +115,7 @@ public class UriGrantsManagerService extends IUriGrantsManager.Stub { private static final String TAG = "UriGrantsManagerService"; // Maximum number of persisted Uri grants a package is allowed private static final int MAX_PERSISTED_URI_GRANTS = 128; - private static final boolean ENABLE_DYNAMIC_PERMISSIONS = false; + private static final boolean ENABLE_DYNAMIC_PERMISSIONS = true; private final Object mLock = new Object(); private final H mH; diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java index b9240c78d711..9bc702d4c094 100644 --- a/services/core/java/com/android/server/wm/AccessibilityController.java +++ b/services/core/java/com/android/server/wm/AccessibilityController.java @@ -855,8 +855,7 @@ final class AccessibilityController { mTempLayer = 0; mDisplayContent.forAllWindows((w) -> { if (w.isOnScreen() && w.isVisibleLw() - && (w.mAttrs.alpha != 0) - && !w.mWinAnimator.mEnterAnimationPending) { + && (w.mAttrs.alpha != 0)) { mTempLayer++; outWindows.put(mTempLayer, w); } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 319fd18faca5..2a68a194b369 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -3556,10 +3556,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A WindowState getImeTargetBelowWindow(WindowState w) { final int index = mChildren.indexOf(w); if (index > 0) { - final WindowState target = mChildren.get(index - 1); - if (target.canBeImeTarget()) { - return target; - } + return mChildren.get(index - 1) + .getWindow(WindowState::canBeImeTarget); } return null; } diff --git a/services/core/java/com/android/server/wm/ActivityStartController.java b/services/core/java/com/android/server/wm/ActivityStartController.java index 6fbfa68d9309..16ca60d1519b 100644 --- a/services/core/java/com/android/server/wm/ActivityStartController.java +++ b/services/core/java/com/android/server/wm/ActivityStartController.java @@ -52,6 +52,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.server.am.ActivityManagerService; import com.android.server.am.PendingIntentRecord; +import com.android.server.uri.NeededUriGrants; import com.android.server.wm.ActivityStackSupervisor.PendingActivityLaunch; import com.android.server.wm.ActivityStarter.DefaultFactory; import com.android.server.wm.ActivityStarter.Factory; @@ -402,6 +403,7 @@ public class ActivityStartController { // potentially acquire activity manager lock that leads to deadlock. for (int i = 0; i < intents.length; i++) { Intent intent = intents[i]; + NeededUriGrants intentGrants = null; // Refuse possible leaked file descriptors. if (intent.hasFileDescriptors()) { @@ -418,6 +420,14 @@ public class ActivityStartController { 0 /* startFlags */, null /* profilerInfo */, userId, filterCallingUid); aInfo = mService.mAmInternal.getActivityInfoForUser(aInfo, userId); + // Carefully collect grants without holding lock + if (aInfo != null) { + intentGrants = mSupervisor.mService.mUgmInternal + .checkGrantUriPermissionFromIntent(intent, filterCallingUid, + aInfo.applicationInfo.packageName, + UserHandle.getUserId(aInfo.applicationInfo.uid)); + } + if (aInfo != null) { if ((aInfo.applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_CANT_SAVE_STATE) != 0) { @@ -433,6 +443,7 @@ public class ActivityStartController { ? options : null; starters[i] = obtainStarter(intent, reason) + .setIntentGrants(intentGrants) .setCaller(caller) .setResolvedType(resolvedTypes[i]) .setActivityInfo(aInfo) diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index daa97b56ed66..31712ef85add 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -639,8 +639,14 @@ class ActivityStarter { mRequest.intent, caller); } - // Do not lock the resolving to avoid potential deadlock. + // If the caller hasn't already resolved the activity, we're willing + // to do so here, but because that may require acquiring the AM lock + // as part of calculating the NeededUriGrants, we must never hold + // the WM lock here to avoid deadlocking. if (mRequest.activityInfo == null) { + if (Thread.holdsLock(mService.mGlobalLock)) { + Slog.wtf(TAG, new IllegalStateException("Caller must not hold WM lock")); + } mRequest.resolveActivity(mSupervisor); } @@ -1662,6 +1668,12 @@ class ActivityStarter { mTargetStack.setAlwaysOnTop(true); } } + if (!mTargetStack.isTopStackInDisplayArea() && mService.mInternal.isDreaming()) { + // Launching underneath dream activity (fullscreen, always-on-top). Run the launch- + // -behind transition so the Activity gets created and starts in visible state. + mLaunchTaskBehind = true; + r.mLaunchTaskBehind = true; + } } mService.mUgmInternal.grantUriPermissionUncheckedFromIntent(intentGrants, @@ -1911,6 +1923,12 @@ class ActivityStarter { return START_SUCCESS; } + // At this point we are certain we want the task moved to the front. If we need to dismiss + // any other always-on-top stacks, now is the time to do it. + if (targetTaskTop.canTurnScreenOn() && mService.mInternal.isDreaming()) { + targetTaskTop.mStackSupervisor.wakeUp("recycleTask#turnScreenOnFlag"); + } + if (mMovedToFront) { // We moved the task to front, use starting window to hide initial drawn delay. targetTaskTop.showStartingWindow(null /* prev */, false /* newTask */, @@ -2632,6 +2650,11 @@ class ActivityStarter { return mRequest.intent; } + ActivityStarter setIntentGrants(NeededUriGrants intentGrants) { + mRequest.intentGrants = intentGrants; + return this; + } + ActivityStarter setReason(String reason) { mRequest.reason = reason; return this; diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index cf453c7feef8..205523b2a119 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -6174,12 +6174,10 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { boolean validateIncomingUser, PendingIntentRecord originatingPendingIntent, boolean allowBackgroundActivityStart) { assertPackageMatchesCallingUid(callingPackage); - synchronized (mGlobalLock) { - return getActivityStartController().startActivitiesInPackage(uid, realCallingPid, - realCallingUid, callingPackage, callingFeatureId, intents, resolvedTypes, - resultTo, options, userId, validateIncomingUser, originatingPendingIntent, - allowBackgroundActivityStart); - } + return getActivityStartController().startActivitiesInPackage(uid, realCallingPid, + realCallingUid, callingPackage, callingFeatureId, intents, resolvedTypes, + resultTo, options, userId, validateIncomingUser, originatingPendingIntent, + allowBackgroundActivityStart); } @Override @@ -6190,13 +6188,11 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { boolean validateIncomingUser, PendingIntentRecord originatingPendingIntent, boolean allowBackgroundActivityStart) { assertPackageMatchesCallingUid(callingPackage); - synchronized (mGlobalLock) { - return getActivityStartController().startActivityInPackage(uid, realCallingPid, - realCallingUid, callingPackage, callingFeatureId, intent, resolvedType, - resultTo, resultWho, requestCode, startFlags, options, userId, inTask, - reason, validateIncomingUser, originatingPendingIntent, - allowBackgroundActivityStart); - } + return getActivityStartController().startActivityInPackage(uid, realCallingPid, + realCallingUid, callingPackage, callingFeatureId, intent, resolvedType, + resultTo, resultWho, requestCode, startFlags, options, userId, inTask, + reason, validateIncomingUser, originatingPendingIntent, + allowBackgroundActivityStart); } @Override diff --git a/services/core/java/com/android/server/wm/AppTaskImpl.java b/services/core/java/com/android/server/wm/AppTaskImpl.java index 8fa811915fc2..dd1d55b2d54d 100644 --- a/services/core/java/com/android/server/wm/AppTaskImpl.java +++ b/services/core/java/com/android/server/wm/AppTaskImpl.java @@ -83,7 +83,8 @@ class AppTaskImpl extends IAppTask.Stub { if (task == null) { throw new IllegalArgumentException("Unable to find task ID " + mTaskId); } - return mService.getRecentTasks().createRecentTaskInfo(task); + return mService.getRecentTasks().createRecentTaskInfo(task, + false /* stripExtras */); } finally { Binder.restoreCallingIdentity(origId); } diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 9b5d94ebb1ac..86ef0d82c1d1 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -1185,6 +1185,9 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo } activity.onRemovedFromDisplay(); + if (activity == mFixedRotationLaunchingApp) { + setFixedRotationLaunchingAppUnchecked(null); + } } @Override @@ -1468,6 +1471,12 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo // It has been set and not yet finished. return true; } + if (!r.occludesParent() || r.isVisible()) { + // While entering or leaving a translucent or floating activity (e.g. dialog style), + // there is a visible activity in the background. Then it still needs rotation animation + // to cover the activity configuration change. + return false; + } if (checkOpening) { if (!mAppTransition.isTransitionSet() || !mOpeningApps.contains(r)) { // Apply normal rotation animation in case of the activity set different requested @@ -3428,9 +3437,10 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo "Proposed new IME target: " + target + " for display: " + getDisplayId()); // Now, a special case -- if the last target's window is in the process of exiting, but - // not removed, keep on the last target to avoid IME flicker. + // not removed, keep on the last target to avoid IME flicker. The exception is if the + // current target is home since we want opening apps to become the IME target right away. if (curTarget != null && !curTarget.mRemoved && curTarget.isDisplayedLw() - && curTarget.isClosing()) { + && curTarget.isClosing() && !curTarget.isActivityTypeHome()) { if (DEBUG_INPUT_METHOD) Slog.v(TAG_WM, "Not changing target till current window is" + " closing and not removed"); return curTarget; @@ -5636,6 +5646,12 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo */ void onStartRecentsAnimation(@NonNull ActivityRecord r) { mAnimatingRecents = r; + if (r.isVisible() && mFocusedApp != null && !mFocusedApp.occludesParent()) { + // The recents activity has shown with the orientation determined by the top + // activity, keep its current orientation to avoid flicking by the configuration + // change of visible activity. + return; + } rotateInDifferentOrientationIfNeeded(r); if (r.hasFixedRotationTransform()) { // Set the record so we can recognize it to continue to update display orientation diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java index 24bb7c8d5560..1b58fc1d2d3e 100644 --- a/services/core/java/com/android/server/wm/RecentTasks.java +++ b/services/core/java/com/android/server/wm/RecentTasks.java @@ -961,7 +961,7 @@ class RecentTasks { continue; } - res.add(createRecentTaskInfo(task)); + res.add(createRecentTaskInfo(task, true /* stripExtras */)); } return res; } @@ -1832,9 +1832,9 @@ class RecentTasks { /** * Creates a new RecentTaskInfo from a Task. */ - ActivityManager.RecentTaskInfo createRecentTaskInfo(Task tr) { + ActivityManager.RecentTaskInfo createRecentTaskInfo(Task tr, boolean stripExtras) { ActivityManager.RecentTaskInfo rti = new ActivityManager.RecentTaskInfo(); - tr.fillTaskInfo(rti); + tr.fillTaskInfo(rti, stripExtras); // Fill in some deprecated values rti.id = rti.isRunning ? rti.taskId : INVALID_TASK_ID; rti.persistentId = rti.taskId; diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index fec484935d68..a7b120942163 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -2934,7 +2934,7 @@ class Task extends WindowContainer<WindowContainer> { final Task rootTask = getRootTask(); final Task topNonOrgTask = rootTask.mCreatedByOrganizer ? rootTask.getTopMostTask() : rootTask; - if (isDescendantOf(topNonOrgTask)) { + if (this == topNonOrgTask || isDescendantOf(topNonOrgTask)) { return false; } } @@ -3551,11 +3551,19 @@ class Task extends WindowContainer<WindowContainer> { } } + /** * Fills in a {@link TaskInfo} with information from this task. Note that the base intent in the * task info will not include any extras or clip data. */ void fillTaskInfo(TaskInfo info) { + fillTaskInfo(info, true /* stripExtras */); + } + + /** + * Fills in a {@link TaskInfo} with information from this task. + */ + void fillTaskInfo(TaskInfo info, boolean stripExtras) { getNumRunningActivities(mReuseActivitiesReport); info.userId = mUserId; info.stackId = getRootTaskId(); @@ -3566,7 +3574,9 @@ class Task extends WindowContainer<WindowContainer> { // Make a copy of base intent because this is like a snapshot info. // Besides, {@link RecentTasks#getRecentTasksImpl} may modify it. final int baseIntentFlags = baseIntent == null ? 0 : baseIntent.getFlags(); - info.baseIntent = baseIntent == null ? new Intent() : baseIntent.cloneFilter(); + info.baseIntent = baseIntent == null + ? new Intent() + : stripExtras ? baseIntent.cloneFilter() : new Intent(baseIntent); info.baseIntent.setFlags(baseIntentFlags); info.baseActivity = mReuseActivitiesReport.base != null ? mReuseActivitiesReport.base.intent.getComponent() diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java index 7b690383f5f9..e23080724000 100644 --- a/services/core/java/com/android/server/wm/TaskDisplayArea.java +++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java @@ -564,16 +564,23 @@ final class TaskDisplayArea extends DisplayArea<ActivityStack> { // Apps and their containers are not allowed to specify an orientation while using // root tasks...except for the home stack if it is not resizable and currently // visible (top of) its root task. - if (mRootHomeTask != null && mRootHomeTask.isVisible() - && !mRootHomeTask.isResizeable()) { + if (mRootHomeTask != null && !mRootHomeTask.isResizeable()) { // Manually nest one-level because because getOrientation() checks fillsParent() // which checks that requestedOverrideBounds() is empty. However, in this case, // it is not empty because it's been overridden to maintain the fullscreen size // within a smaller split-root. final Task topHomeTask = mRootHomeTask.getTopMostTask(); - final int orientation = topHomeTask.getOrientation(); - if (orientation != SCREEN_ORIENTATION_UNSET) { - return orientation; + final ActivityRecord topHomeActivity = topHomeTask.getTopNonFinishingActivity(); + // If a home activity is in the process of launching and isn't yet visible we + // should still respect the stack's preferred orientation to ensure rotation occurs + // before the home activity finishes launching. + final boolean isHomeActivityLaunching = topHomeActivity != null + && topHomeActivity.mVisibleRequested; + if (topHomeTask.isVisible() || isHomeActivityLaunching) { + final int orientation = topHomeTask.getOrientation(); + if (orientation != SCREEN_ORIENTATION_UNSET) { + return orientation; + } } } return SCREEN_ORIENTATION_UNSPECIFIED; diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 52fb941ebfc0..564eecf725cc 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -5142,8 +5142,8 @@ public class WindowManagerService extends IWindowManager.Stub } case WINDOW_STATE_BLAST_SYNC_TIMEOUT: { synchronized (mGlobalLock) { - final WindowState ws = (WindowState) msg.obj; - ws.finishDrawing(null); + final WindowState ws = (WindowState) msg.obj; + ws.immediatelyNotifyBlastSync(); } break; } diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index fbc5afadac6b..46e1bf04bdbe 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -428,6 +428,9 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub try { callback.onTransactionReady(mSyncId, mergedTransaction); } catch (RemoteException e) { + // If there's an exception when trying to send the mergedTransaction to the client, we + // should immediately apply it here so the transactions aren't lost. + mergedTransaction.apply(); } mTransactionCallbacksByPendingSyncId.remove(mSyncId); diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 26a1fea1732b..708abe28c806 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -2192,7 +2192,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP void removeIfPossible() { super.removeIfPossible(); removeIfPossible(false /*keepVisibleDeadWindow*/); - finishDrawing(null); + immediatelyNotifyBlastSync(); } private void removeIfPossible(boolean keepVisibleDeadWindow) { @@ -2385,23 +2385,23 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP return false; } - if (PixelFormat.formatHasAlpha(mAttrs.format)) { + if (mAttrs.type == TYPE_APPLICATION_STARTING) { + // Ignore mayUseInputMethod for starting window for now. + // TODO(b/159911356): Remove this special casing (originally added in commit e75d872). + } else if (PixelFormat.formatHasAlpha(mAttrs.format)) { // Support legacy use cases where transparent windows can still be ime target with // FLAG_NOT_FOCUSABLE and ALT_FOCUSABLE_IM set. // Certain apps listen for IME insets using transparent windows and ADJUST_NOTHING to // manually synchronize app content to IME animation b/144619551. // TODO(b/145812508): remove this once new focus management is complete b/141738570 final int fl = mAttrs.flags & (FLAG_NOT_FOCUSABLE | FLAG_ALT_FOCUSABLE_IM); - final int type = mAttrs.type; // Can only be an IME target if both FLAG_NOT_FOCUSABLE and FLAG_ALT_FOCUSABLE_IM are // set or both are cleared...and not a starting window. - if (fl != 0 && fl != (FLAG_NOT_FOCUSABLE | FLAG_ALT_FOCUSABLE_IM) - && type != TYPE_APPLICATION_STARTING) { + if (fl != 0 && fl != (FLAG_NOT_FOCUSABLE | FLAG_ALT_FOCUSABLE_IM)) { return false; } - } else if (!WindowManager.LayoutParams.mayUseInputMethod(mAttrs.flags) - || mAttrs.type == TYPE_APPLICATION_STARTING) { + } else if (!WindowManager.LayoutParams.mayUseInputMethod(mAttrs.flags)) { // Can be an IME target only if: // 1. FLAG_NOT_FOCUSABLE is not set // 2. FLAG_ALT_FOCUSABLE_IM is not set @@ -2696,7 +2696,9 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP Settings.Global.THEATER_MODE_ON, 0) == 0; boolean canTurnScreenOn = mActivityRecord == null || mActivityRecord.currentLaunchCanTurnScreenOn(); - if (allowTheaterMode && canTurnScreenOn && !mPowerManagerWrapper.isInteractive()) { + if (allowTheaterMode && canTurnScreenOn + && (mWmService.mAtmInternal.isDreaming() + || !mPowerManagerWrapper.isInteractive())) { if (DEBUG_VISIBILITY || DEBUG_POWER) { Slog.v(TAG, "Relayout window turning screen on: " + this); } @@ -5806,7 +5808,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP // client will not render when visibility is GONE. Therefore, call finishDrawing here to // prevent system server from blocking on a window that will not draw. if (viewVisibility == View.GONE && mUsingBLASTSyncTransaction) { - finishDrawing(null); + immediatelyNotifyBlastSync(); } } @@ -5844,7 +5846,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP return mWinAnimator.finishDrawingLocked(postDrawTransaction); } - mWmService.mH.removeMessages(WINDOW_STATE_BLAST_SYNC_TIMEOUT, this); if (postDrawTransaction != null) { mBLASTSyncTransaction.merge(postDrawTransaction); } @@ -5853,8 +5854,9 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP return mWinAnimator.finishDrawingLocked(null); } - @VisibleForTesting - void notifyBlastSyncTransaction() { + private void notifyBlastSyncTransaction() { + mWmService.mH.removeMessages(WINDOW_STATE_BLAST_SYNC_TIMEOUT, this); + if (!mNotifyBlastOnSurfacePlacement || mWaitingListener == null) { mNotifyBlastOnSurfacePlacement = false; return; @@ -5877,6 +5879,11 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP mNotifyBlastOnSurfacePlacement = false; } + void immediatelyNotifyBlastSync() { + finishDrawing(null); + notifyBlastSyncTransaction(); + } + private boolean requestResizeForBlastSync() { return useBLASTSync() && !mResizeForBlastSyncReported; } diff --git a/services/tests/PackageManagerServiceTests/OWNERS b/services/tests/PackageManagerServiceTests/OWNERS new file mode 100644 index 000000000000..182dfe8fca9e --- /dev/null +++ b/services/tests/PackageManagerServiceTests/OWNERS @@ -0,0 +1,3 @@ +chiuwinson@google.com +patb@google.com +toddke@google.com diff --git a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/HostUtils.kt b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/HostUtils.kt index 490f96d8f426..234fcf19062d 100644 --- a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/HostUtils.kt +++ b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/HostUtils.kt @@ -22,10 +22,16 @@ import java.io.File import java.io.FileOutputStream internal fun SystemPreparer.pushApk(file: String, partition: Partition) = - pushResourceFile(file, HostUtils.makePathForApk(file, partition)) - -internal fun SystemPreparer.deleteApk(file: String, partition: Partition) = - deleteFile(partition.baseFolder.resolve(file.removeSuffix(".apk")).toString()) + pushResourceFile(file, HostUtils.makePathForApk(file, partition).toString()) + +internal fun SystemPreparer.deleteApkFolders( + partition: Partition, + vararg javaResourceNames: String +) = apply { + javaResourceNames.forEach { + deleteFile(partition.baseAppFolder.resolve(it.removeSuffix(".apk")).toString()) + } +} internal object HostUtils { @@ -40,10 +46,9 @@ internal object HostUtils { makePathForApk(File(fileName), partition) fun makePathForApk(file: File, partition: Partition) = - partition.baseFolder + partition.baseAppFolder .resolve(file.nameWithoutExtension) .resolve(file.name) - .toString() fun copyResourceToHostFile(javaResourceName: String, file: File): File { javaClass.classLoader!!.getResource(javaResourceName).openStream().use { input -> diff --git a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/InvalidNewSystemAppTest.kt b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/InvalidNewSystemAppTest.kt index 98e045d0a203..39b40d8d3195 100644 --- a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/InvalidNewSystemAppTest.kt +++ b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/InvalidNewSystemAppTest.kt @@ -45,23 +45,24 @@ class InvalidNewSystemAppTest : BaseHostJUnit4Test() { private val tempFolder = TemporaryFolder() private val preparer: SystemPreparer = SystemPreparer(tempFolder, - SystemPreparer.RebootStrategy.START_STOP, deviceRebootRule) { this.device } + SystemPreparer.RebootStrategy.FULL, deviceRebootRule) { this.device } @get:Rule val rules = RuleChain.outerRule(tempFolder).around(preparer)!! + private val filePath = HostUtils.makePathForApk("PackageManagerDummyApp.apk", Partition.PRODUCT) @Before @After - fun uninstallDataPackage() { + fun removeApk() { device.uninstallPackage(TEST_PKG_NAME) + device.deleteFile(filePath.parent.toString()) + device.reboot() } @Test fun verify() { // First, push a system app to the device and then update it so there's a data variant - val filePath = HostUtils.makePathForApk("PackageManagerDummyApp.apk", Partition.PRODUCT) - - preparer.pushResourceFile(VERSION_ONE, filePath) + preparer.pushResourceFile(VERSION_ONE, filePath.toString()) .reboot() val versionTwoFile = HostUtils.copyResourceToHostFile(VERSION_TWO, tempFolder.newFile()) @@ -69,8 +70,8 @@ class InvalidNewSystemAppTest : BaseHostJUnit4Test() { assertThat(device.installPackage(versionTwoFile, true)).isNull() // Then push a bad update to the system, overwriting the existing file as if an OTA occurred - preparer.deleteFile(filePath) - .pushResourceFile(VERSION_THREE_INVALID, filePath) + preparer.deleteFile(filePath.toString()) + .pushResourceFile(VERSION_THREE_INVALID, filePath.toString()) .reboot() // This will remove the package from the device, which is expected diff --git a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/OriginalPackageMigrationTest.kt b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/OriginalPackageMigrationTest.kt index 90494c591363..fb0348c3146b 100644 --- a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/OriginalPackageMigrationTest.kt +++ b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/OriginalPackageMigrationTest.kt @@ -20,6 +20,8 @@ import com.android.internal.util.test.SystemPreparer import com.android.tradefed.testtype.DeviceJUnit4ClassRunner import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before import org.junit.ClassRule import org.junit.Rule import org.junit.Test @@ -43,11 +45,18 @@ class OriginalPackageMigrationTest : BaseHostJUnit4Test() { private val tempFolder = TemporaryFolder() private val preparer: SystemPreparer = SystemPreparer(tempFolder, - SystemPreparer.RebootStrategy.START_STOP, deviceRebootRule) { this.device } + SystemPreparer.RebootStrategy.FULL, deviceRebootRule) { this.device } @get:Rule val rules = RuleChain.outerRule(tempFolder).around(preparer)!! + @Before + @After + fun deleteApkFolders() { + preparer.deleteApkFolders(Partition.SYSTEM, VERSION_ONE, VERSION_TWO, VERSION_THREE, + NEW_PKG) + } + @Test fun lowerVersion() { runForApk(VERSION_ONE) @@ -71,28 +80,28 @@ class OriginalPackageMigrationTest : BaseHostJUnit4Test() { preparer.pushApk(apk, Partition.SYSTEM) .reboot() - device.getAppPackageInfo(TEST_PKG_NAME).run { - assertThat(codePath).contains(apk.removeSuffix(".apk")) - } + assertCodePath(apk) // Ensure data is preserved by writing to the original dataDir val file = tempFolder.newFile().apply { writeText("Test") } device.pushFile(file, "${HostUtils.getDataDir(device, TEST_PKG_NAME)}/files/test.txt") - preparer.deleteApk(apk, Partition.SYSTEM) + preparer.deleteApkFolders(Partition.SYSTEM, apk) .pushApk(NEW_PKG, Partition.SYSTEM) .reboot() - device.getAppPackageInfo(TEST_PKG_NAME) - .run { - assertThat(this.toString()).isNotEmpty() - assertThat(codePath) - .contains(NEW_PKG.removeSuffix(".apk")) - } + assertCodePath(NEW_PKG) // And then reading the data contents back assertThat(device.pullFileContents( "${HostUtils.getDataDir(device, TEST_PKG_NAME)}/files/test.txt")) .isEqualTo("Test") } + + private fun assertCodePath(apk: String) { + // dumpsys package and therefore device.getAppPackageInfo doesn't work here for some reason, + // so parse the package dump directly to see if the path matches. + assertThat(device.executeShellCommand("pm dump $TEST_PKG_NAME")) + .contains(HostUtils.makePathForApk(apk, Partition.SYSTEM).parent.toString()) + } } diff --git a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/Partition.kt b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/Partition.kt index 35192a73ceda..654c11c5bf81 100644 --- a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/Partition.kt +++ b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/Partition.kt @@ -20,7 +20,7 @@ import java.nio.file.Path import java.nio.file.Paths // Unfortunately no easy way to access PMS SystemPartitions, so mock them here -internal enum class Partition(val baseFolder: Path) { +internal enum class Partition(val baseAppFolder: Path) { SYSTEM("/system/app"), VENDOR("/vendor/app"), PRODUCT("/product/app"), diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java index cdafd32cbbb5..fde40aa77a0e 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java @@ -63,7 +63,6 @@ import static com.android.server.am.ProcessList.VISIBLE_APP_ADJ; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.AdditionalAnswers.answer; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.anyLong; @@ -171,7 +170,6 @@ public class MockingOomAdjusterTests { mock(OomAdjProfiler.class)); doReturn(new ActivityManagerService.ProcessChangeItem()).when(sService) .enqueueProcessChangeItemLocked(anyInt(), anyInt()); - doReturn(true).when(sService).containsTopUiOrRunningRemoteAnimOrEmptyLocked(any()); sService.mOomAdjuster = new OomAdjuster(sService, sService.mProcessList, mock(ActiveUids.class)); sService.mOomAdjuster.mAdjSeq = 10000; @@ -268,21 +266,6 @@ public class MockingOomAdjusterTests { @SuppressWarnings("GuardedBy") @Test - public void testUpdateOomAdj_DoOne_TopApp_PreemptedByTopUi() { - ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID, - MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true)); - doReturn(PROCESS_STATE_TOP).when(sService.mAtmInternal).getTopProcessState(); - doReturn(app).when(sService).getTopAppLocked(); - doReturn(false).when(sService).containsTopUiOrRunningRemoteAnimOrEmptyLocked(eq(app)); - sService.mWakefulness = PowerManagerInternal.WAKEFULNESS_AWAKE; - sService.mOomAdjuster.updateOomAdjLocked(app, false, OomAdjuster.OOM_ADJ_REASON_NONE); - doReturn(null).when(sService).getTopAppLocked(); - - assertProcStates(app, PROCESS_STATE_TOP, FOREGROUND_APP_ADJ, SCHED_GROUP_DEFAULT); - } - - @SuppressWarnings("GuardedBy") - @Test public void testUpdateOomAdj_DoOne_RunningInstrumentation() { ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID, MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true)); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java index e3453a06990f..b2847ce88a2c 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java @@ -23,6 +23,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.app.ActivityManager; @@ -81,8 +82,10 @@ public class PendingIntentControllerTest { doReturn(mIPackageManager).when(() -> AppGlobals.getPackageManager()); when(mIPackageManager.getPackageUid(eq(TEST_PACKAGE_NAME), anyInt(), anyInt())).thenReturn( TEST_CALLING_UID); + ActivityManagerConstants constants = mock(ActivityManagerConstants.class); + constants.PENDINGINTENT_WARNING_THRESHOLD = 2000; mPendingIntentController = new PendingIntentController(Looper.getMainLooper(), - mUserController); + mUserController, constants); mPendingIntentController.onActivityManagerInternalAdded(); } diff --git a/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java index 7446289cd498..924ad7f3f5a1 100644 --- a/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java @@ -174,10 +174,10 @@ public class BlobStoreManagerServiceTest { mService.handlePackageRemoved(TEST_PKG1, TEST_UID1); // Verify sessions are removed - verify(sessionFile1).delete(); - verify(sessionFile2, never()).delete(); - verify(sessionFile3, never()).delete(); - verify(sessionFile4).delete(); + verify(session1).destroy(); + verify(session2, never()).destroy(); + verify(session3, never()).destroy(); + verify(session4).destroy(); assertThat(mUserSessions.size()).isEqualTo(2); assertThat(mUserSessions.get(sessionId1)).isNull(); @@ -193,9 +193,9 @@ public class BlobStoreManagerServiceTest { verify(blobMetadata3).removeCommitter(TEST_PKG1, TEST_UID1); verify(blobMetadata3).removeLeasee(TEST_PKG1, TEST_UID1); - verify(blobFile1, never()).delete(); - verify(blobFile2).delete(); - verify(blobFile3).delete(); + verify(blobMetadata1, never()).destroy(); + verify(blobMetadata2).destroy(); + verify(blobMetadata3).destroy(); assertThat(mUserBlobs.size()).isEqualTo(1); assertThat(mUserBlobs.get(blobHandle1)).isNotNull(); @@ -272,9 +272,9 @@ public class BlobStoreManagerServiceTest { mService.handleIdleMaintenanceLocked(); // Verify stale sessions are removed - verify(sessionFile1).delete(); - verify(sessionFile2, never()).delete(); - verify(sessionFile3).delete(); + verify(session1).destroy(); + verify(session2, never()).destroy(); + verify(session3).destroy(); assertThat(mUserSessions.size()).isEqualTo(1); assertThat(mUserSessions.get(sessionId2)).isNotNull(); @@ -317,9 +317,9 @@ public class BlobStoreManagerServiceTest { mService.handleIdleMaintenanceLocked(); // Verify stale blobs are removed - verify(blobFile1).delete(); - verify(blobFile2, never()).delete(); - verify(blobFile3).delete(); + verify(blobMetadata1).destroy(); + verify(blobMetadata2, never()).destroy(); + verify(blobMetadata3).destroy(); assertThat(mUserBlobs.size()).isEqualTo(1); assertThat(mUserBlobs.get(blobHandle2)).isNotNull(); @@ -377,11 +377,11 @@ public class BlobStoreManagerServiceTest { } private BlobMetadata createBlobMetadataMock(long blobId, File blobFile, - BlobHandle blobHandle, boolean hasLeases) { + BlobHandle blobHandle, boolean hasValidLeases) { final BlobMetadata blobMetadata = mock(BlobMetadata.class); doReturn(blobId).when(blobMetadata).getBlobId(); doReturn(blobFile).when(blobMetadata).getBlobFile(); - doReturn(hasLeases).when(blobMetadata).hasLeases(); + doReturn(hasValidLeases).when(blobMetadata).hasValidLeases(); doReturn(blobHandle).when(blobMetadata).getBlobHandle(); doCallRealMethod().when(blobMetadata).shouldBeDeleted(anyBoolean()); doReturn(true).when(blobMetadata).hasLeaseWaitTimeElapsedForAll(); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/FullScreenMagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/FullScreenMagnificationGestureHandlerTest.java index 2007d4fff8c1..1cbee12720b0 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/FullScreenMagnificationGestureHandlerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/FullScreenMagnificationGestureHandlerTest.java @@ -20,6 +20,7 @@ import static android.view.MotionEvent.ACTION_DOWN; import static android.view.MotionEvent.ACTION_MOVE; import static android.view.MotionEvent.ACTION_POINTER_DOWN; import static android.view.MotionEvent.ACTION_POINTER_UP; +import static android.view.MotionEvent.ACTION_UP; import static com.android.server.testutils.TestUtils.strictMock; @@ -38,11 +39,13 @@ import static org.mockito.Mockito.when; import android.animation.ValueAnimator; import android.annotation.NonNull; import android.content.Context; +import android.graphics.PointF; import android.os.Handler; import android.os.Message; import android.util.DebugUtils; import android.view.InputDevice; import android.view.MotionEvent; +import android.view.ViewConfiguration; import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; @@ -56,6 +59,9 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; import java.util.function.IntConsumer; /** @@ -106,6 +112,7 @@ public class FullScreenMagnificationGestureHandlerTest { // Co-prime x and y, to potentially catch x-y-swapped errors public static final float DEFAULT_X = 301; public static final float DEFAULT_Y = 299; + public static final PointF DEFAULT_POINT = new PointF(DEFAULT_X, DEFAULT_Y); private static final int DISPLAY_0 = 0; @@ -327,6 +334,107 @@ public class FullScreenMagnificationGestureHandlerTest { }); } + @Test + public void testTwoFingersOneTap_zoomedState_dispatchMotionEvents() { + goFromStateIdleTo(STATE_ZOOMED); + final EventCaptor eventCaptor = new EventCaptor(); + mMgh.setNext(eventCaptor); + + send(downEvent()); + send(pointerEvent(ACTION_POINTER_DOWN, DEFAULT_X * 2, DEFAULT_Y)); + send(pointerEvent(ACTION_POINTER_UP, DEFAULT_X * 2, DEFAULT_Y)); + send(upEvent()); + + assertIn(STATE_ZOOMED); + final List<Integer> expectedActions = new ArrayList(); + expectedActions.add(Integer.valueOf(ACTION_DOWN)); + expectedActions.add(Integer.valueOf(ACTION_POINTER_DOWN)); + expectedActions.add(Integer.valueOf(ACTION_POINTER_UP)); + expectedActions.add(Integer.valueOf(ACTION_UP)); + assertActionsInOrder(eventCaptor.mEvents, expectedActions); + + returnToNormalFrom(STATE_ZOOMED); + } + + @Test + public void testThreeFingersOneTap_zoomedState_dispatchMotionEvents() { + goFromStateIdleTo(STATE_ZOOMED); + final EventCaptor eventCaptor = new EventCaptor(); + mMgh.setNext(eventCaptor); + PointF pointer1 = DEFAULT_POINT; + PointF pointer2 = new PointF(DEFAULT_X * 1.5f, DEFAULT_Y); + PointF pointer3 = new PointF(DEFAULT_X * 2, DEFAULT_Y); + + send(downEvent()); + send(pointerEvent(ACTION_POINTER_DOWN, new PointF[] {pointer1, pointer2})); + send(pointerEvent(ACTION_POINTER_DOWN, new PointF[] {pointer1, pointer2, pointer3})); + send(pointerEvent(ACTION_POINTER_UP, new PointF[] {pointer1, pointer2, pointer3})); + send(pointerEvent(ACTION_POINTER_UP, new PointF[] {pointer1, pointer2, pointer3})); + send(upEvent()); + + assertIn(STATE_ZOOMED); + final List<Integer> expectedActions = new ArrayList(); + expectedActions.add(Integer.valueOf(ACTION_DOWN)); + expectedActions.add(Integer.valueOf(ACTION_POINTER_DOWN)); + expectedActions.add(Integer.valueOf(ACTION_POINTER_DOWN)); + expectedActions.add(Integer.valueOf(ACTION_POINTER_UP)); + expectedActions.add(Integer.valueOf(ACTION_POINTER_UP)); + expectedActions.add(Integer.valueOf(ACTION_UP)); + assertActionsInOrder(eventCaptor.mEvents, expectedActions); + + returnToNormalFrom(STATE_ZOOMED); + } + + @Test + public void testFirstFingerSwipe_TwoPinterDownAndZoomedState_panningState() { + goFromStateIdleTo(STATE_ZOOMED); + PointF pointer1 = DEFAULT_POINT; + PointF pointer2 = new PointF(DEFAULT_X * 1.5f, DEFAULT_Y); + + send(downEvent()); + send(pointerEvent(ACTION_POINTER_DOWN, new PointF[] {pointer1, pointer2})); + //The minimum movement to transit to panningState. + final float sWipeMinDistance = ViewConfiguration.get(mContext).getScaledTouchSlop(); + pointer1.offset(sWipeMinDistance + 1, 0); + send(pointerEvent(ACTION_MOVE, new PointF[] {pointer1, pointer2})); + assertIn(STATE_PANNING); + + assertIn(STATE_PANNING); + returnToNormalFrom(STATE_PANNING); + } + + @Test + public void testSecondFingerSwipe_TwoPinterDownAndZoomedState_panningState() { + goFromStateIdleTo(STATE_ZOOMED); + PointF pointer1 = DEFAULT_POINT; + PointF pointer2 = new PointF(DEFAULT_X * 1.5f, DEFAULT_Y); + + send(downEvent()); + send(pointerEvent(ACTION_POINTER_DOWN, new PointF[] {pointer1, pointer2})); + //The minimum movement to transit to panningState. + final float sWipeMinDistance = ViewConfiguration.get(mContext).getScaledTouchSlop(); + pointer2.offset(sWipeMinDistance + 1, 0); + send(pointerEvent(ACTION_MOVE, new PointF[] {pointer1, pointer2})); + assertIn(STATE_PANNING); + + assertIn(STATE_PANNING); + returnToNormalFrom(STATE_PANNING); + } + + private void assertActionsInOrder(List<MotionEvent> actualEvents, + List<Integer> expectedActions) { + assertTrue(actualEvents.size() == expectedActions.size()); + final int size = actualEvents.size(); + for (int i = 0; i < size; i++) { + final int expectedAction = expectedActions.get(i); + final int actualAction = actualEvents.get(i).getActionMasked(); + assertTrue(String.format( + "%dth action %s is not matched, actual events : %s, ", i, + MotionEvent.actionToString(expectedAction), actualEvents), + actualAction == expectedAction); + } + } + private void assertZoomsImmediatelyOnSwipeFrom(int state) { goFromStateIdleTo(state); swipeAndHold(); @@ -467,6 +575,7 @@ public class FullScreenMagnificationGestureHandlerTest { goFromStateIdleTo(STATE_ZOOMED); send(downEvent()); send(pointerEvent(ACTION_POINTER_DOWN, DEFAULT_X * 2, DEFAULT_Y)); + fastForward(ViewConfiguration.getTapTimeout()); } break; case STATE_SCALING_AND_PANNING: { goFromStateIdleTo(STATE_PANNING); @@ -619,40 +728,67 @@ public class FullScreenMagnificationGestureHandlerTest { MotionEvent.ACTION_UP, x, y, 0)); } + private MotionEvent pointerEvent(int action, float x, float y) { - MotionEvent.PointerProperties defPointerProperties = new MotionEvent.PointerProperties(); - defPointerProperties.id = 0; - defPointerProperties.toolType = MotionEvent.TOOL_TYPE_FINGER; - MotionEvent.PointerProperties pointerProperties = new MotionEvent.PointerProperties(); - pointerProperties.id = 1; - pointerProperties.toolType = MotionEvent.TOOL_TYPE_FINGER; - - MotionEvent.PointerCoords defPointerCoords = new MotionEvent.PointerCoords(); - defPointerCoords.x = DEFAULT_X; - defPointerCoords.y = DEFAULT_Y; - MotionEvent.PointerCoords pointerCoords = new MotionEvent.PointerCoords(); - pointerCoords.x = x; - pointerCoords.y = y; + return pointerEvent(action, new PointF[] {DEFAULT_POINT, new PointF(x, y)}); + } + + private MotionEvent pointerEvent(int action, PointF[] pointersPosition) { + final MotionEvent.PointerProperties[] PointerPropertiesArray = + new MotionEvent.PointerProperties[pointersPosition.length]; + for (int i = 0; i < pointersPosition.length; i++) { + MotionEvent.PointerProperties pointerProperties = new MotionEvent.PointerProperties(); + pointerProperties.id = i; + pointerProperties.toolType = MotionEvent.TOOL_TYPE_FINGER; + PointerPropertiesArray[i] = pointerProperties; + } + + final MotionEvent.PointerCoords[] pointerCoordsArray = + new MotionEvent.PointerCoords[pointersPosition.length]; + for (int i = 0; i < pointersPosition.length; i++) { + MotionEvent.PointerCoords pointerCoords = new MotionEvent.PointerCoords(); + pointerCoords.x = pointersPosition[i].x; + pointerCoords.y = pointersPosition[i].y; + pointerCoordsArray[i] = pointerCoords; + } return MotionEvent.obtain( - /* downTime */ mClock.now(), - /* eventTime */ mClock.now(), - /* action */ action, - /* pointerCount */ 2, - /* pointerProperties */ new MotionEvent.PointerProperties[] { - defPointerProperties, pointerProperties}, - /* pointerCoords */ new MotionEvent.PointerCoords[] { defPointerCoords, pointerCoords }, - /* metaState */ 0, - /* buttonState */ 0, - /* xPrecision */ 1.0f, - /* yPrecision */ 1.0f, - /* deviceId */ 0, - /* edgeFlags */ 0, - /* source */ InputDevice.SOURCE_TOUCHSCREEN, - /* flags */ 0); + /* downTime */ mClock.now(), + /* eventTime */ mClock.now(), + /* action */ action, + /* pointerCount */ pointersPosition.length, + /* pointerProperties */ PointerPropertiesArray, + /* pointerCoords */ pointerCoordsArray, + /* metaState */ 0, + /* buttonState */ 0, + /* xPrecision */ 1.0f, + /* yPrecision */ 1.0f, + /* deviceId */ 0, + /* edgeFlags */ 0, + /* source */ InputDevice.SOURCE_TOUCHSCREEN, + /* flags */ 0); } + private String stateDump() { return "\nCurrent state dump:\n" + mMgh + "\n" + mHandler.getPendingMessages(); } + + private class EventCaptor implements EventStreamTransformation { + List<MotionEvent> mEvents = new ArrayList<>(); + + @Override + public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + mEvents.add(event.copy()); + } + + @Override + public void setNext(EventStreamTransformation next) { + } + + @Override + public EventStreamTransformation getNext() { + return null; + } + } } diff --git a/services/tests/servicestests/src/com/android/server/hdmi/FakeNativeWrapper.java b/services/tests/servicestests/src/com/android/server/hdmi/FakeNativeWrapper.java index 8607ec66a5ba..7538468fbe31 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/FakeNativeWrapper.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/FakeNativeWrapper.java @@ -17,7 +17,6 @@ package com.android.server.hdmi; import android.hardware.hdmi.HdmiPortInfo; import android.hardware.tv.cec.V1_0.SendMessageResult; -import android.os.MessageQueue; import com.android.internal.annotations.VisibleForTesting; import com.android.server.hdmi.HdmiCecController.NativeWrapper; @@ -53,13 +52,16 @@ final class FakeNativeWrapper implements NativeWrapper { private HdmiPortInfo[] mHdmiPortInfo = null; @Override - public long nativeInit(HdmiCecController handler, MessageQueue messageQueue) { - return 1L; + public String nativeInit() { + return "[class or subclass of IHdmiCec]@Proxy"; } @Override + public void setCallback(HdmiCecController.HdmiCecCallback callback) {} + + @Override public int nativeSendCecCommand( - long controllerPtr, int srcAddress, int dstAddress, byte[] body) { + int srcAddress, int dstAddress, byte[] body) { if (body.length == 0) { return mPollAddressResponse[dstAddress]; } else { @@ -69,30 +71,30 @@ final class FakeNativeWrapper implements NativeWrapper { } @Override - public int nativeAddLogicalAddress(long controllerPtr, int logicalAddress) { + public int nativeAddLogicalAddress(int logicalAddress) { return 0; } @Override - public void nativeClearLogicalAddress(long controllerPtr) {} + public void nativeClearLogicalAddress() {} @Override - public int nativeGetPhysicalAddress(long controllerPtr) { + public int nativeGetPhysicalAddress() { return mMyPhysicalAddress; } @Override - public int nativeGetVersion(long controllerPtr) { + public int nativeGetVersion() { return 0; } @Override - public int nativeGetVendorId(long controllerPtr) { + public int nativeGetVendorId() { return 0; } @Override - public HdmiPortInfo[] nativeGetPortInfos(long controllerPtr) { + public HdmiPortInfo[] nativeGetPortInfos() { if (mHdmiPortInfo == null) { mHdmiPortInfo = new HdmiPortInfo[1]; mHdmiPortInfo[0] = new HdmiPortInfo(1, 1, 0x1000, true, true, true); @@ -101,16 +103,16 @@ final class FakeNativeWrapper implements NativeWrapper { } @Override - public void nativeSetOption(long controllerPtr, int flag, boolean enabled) {} + public void nativeSetOption(int flag, boolean enabled) {} @Override - public void nativeSetLanguage(long controllerPtr, String language) {} + public void nativeSetLanguage(String language) {} @Override - public void nativeEnableAudioReturnChannel(long controllerPtr, int port, boolean flag) {} + public void nativeEnableAudioReturnChannel(int port, boolean flag) {} @Override - public boolean nativeIsConnected(long controllerPtr, int port) { + public boolean nativeIsConnected(int port) { return false; } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index ef4d5db2f32f..16aa87b3e59c 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -1339,14 +1339,15 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @Ignore public void testPostCancelPostNotifiesListeners() throws Exception { // WHEN a notification is posted final StatusBarNotification sbn = generateNotificationRecord(null).getSbn(); mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", sbn.getId(), sbn.getNotification(), sbn.getUserId()); + Thread.sleep(1); // make sure the system clock advances before the next step // THEN it is canceled mBinderService.cancelNotificationWithTag(PKG, PKG, "tag", sbn.getId(), sbn.getUserId()); + Thread.sleep(1); // here too // THEN it is posted again (before the cancel has a chance to finish) mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", sbn.getId(), sbn.getNotification(), sbn.getUserId()); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index 2171d75256f2..668f04785bbc 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -1506,6 +1506,7 @@ public class ActivityRecordTests extends ActivityTestsBase { .setRotation((mActivity.getWindowConfiguration().getRotation() + 1) % 4) .build(); setRotatedScreenOrientationSilently(mActivity); + mActivity.setVisible(false); final IWindowSession session = WindowManagerGlobal.getWindowSession(); spyOn(session); diff --git a/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenTests.java b/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenTests.java index 4dbf79a4a5a6..1d13788bc523 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenTests.java @@ -456,6 +456,7 @@ public class AppWindowTokenTests extends WindowTestsBase { @Test public void testTransferStartingWindowSetFixedRotation() { final ActivityRecord topActivity = createTestActivityRecordForGivenTask(mTask); + topActivity.setVisible(false); mTask.positionChildAt(topActivity, POSITION_TOP); mActivity.addStartingWindow(mPackageName, android.R.style.Theme, null, "Test", 0, 0, 0, 0, null, true, true, false, true, diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index 689674011d30..30af1d34f558 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -43,6 +43,7 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMA import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE; import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; @@ -273,6 +274,38 @@ public class DisplayContentTests extends WindowTestsBase { assertEquals(childWin, imeTarget); } + @Test + public void testComputeImeTarget_startingWindow() { + ActivityRecord activity = createActivityRecord(mDisplayContent, + WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD); + + final WindowState startingWin = createWindow(null, TYPE_APPLICATION_STARTING, activity, + "startingWin"); + startingWin.setHasSurface(true); + assertTrue(startingWin.canBeImeTarget()); + + WindowState imeTarget = mDisplayContent.computeImeTarget(false /* updateImeTarget */); + assertEquals(startingWin, imeTarget); + startingWin.mHidden = false; + + // Verify that an app window launching behind the starting window becomes the target + final WindowState appWin = createWindow(null, TYPE_BASE_APPLICATION, activity, "appWin"); + appWin.setHasSurface(true); + assertTrue(appWin.canBeImeTarget()); + + imeTarget = mDisplayContent.computeImeTarget(false /* updateImeTarget */); + assertEquals(appWin, imeTarget); + appWin.mHidden = false; + + // Verify that an child window can be an ime target even behind a launching app window + final WindowState childWin = createWindow(appWin, + TYPE_APPLICATION_ATTACHED_DIALOG, "childWin"); + childWin.setHasSurface(true); + assertTrue(childWin.canBeImeTarget()); + imeTarget = mDisplayContent.computeImeTarget(false /* updateImeTarget */); + assertEquals(childWin, imeTarget); + } + /** * This tests stack movement between displays and proper stack's, task's and app token's display * container references updates. @@ -1081,6 +1114,7 @@ public class DisplayContentTests extends WindowTestsBase { mDisplayContent.onRequestedOverrideConfigurationChanged(config); final ActivityRecord app = mAppWindow.mActivityRecord; + app.setVisible(false); mDisplayContent.prepareAppTransition(WindowManager.TRANSIT_ACTIVITY_OPEN, false /* alwaysKeepCurrent */); mDisplayContent.mOpeningApps.add(app); @@ -1135,6 +1169,7 @@ public class DisplayContentTests extends WindowTestsBase { // Launch another activity before the transition is finished. final ActivityRecord app2 = new ActivityTestsBase.StackBuilder(mWm.mRoot) .setDisplay(mDisplayContent).build().getTopMostActivity(); + app2.setVisible(false); mDisplayContent.mOpeningApps.add(app2); app2.setRequestedOrientation(newOrientation); @@ -1277,6 +1312,14 @@ public class DisplayContentTests extends WindowTestsBase { mDisplayContent.setFixedRotationLaunchingAppUnchecked(mAppWindow.mActivityRecord); displayRotation.setRotation((displayRotation.getRotation() + 1) % 4); assertTrue(displayRotation.updateRotationUnchecked(false)); + + // The recents activity should not apply fixed rotation if the top activity is not opaque. + mDisplayContent.mFocusedApp = mAppWindow.mActivityRecord; + doReturn(false).when(mDisplayContent.mFocusedApp).occludesParent(); + doReturn(ROTATION_90).when(mDisplayContent).rotationForActivityInDifferentOrientation( + eq(recentsActivity)); + mDisplayContent.mFixedRotationTransitionListener.onStartRecentsAnimation(recentsActivity); + assertFalse(recentsActivity.hasFixedRotationTransform()); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index 130e5550b2a2..3c98272311f7 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -517,6 +517,7 @@ public class SizeCompatTests extends ActivityTestsBase { setUpApp(new TestDisplayContent.Builder(mService, dw, dh).setNotch(notchHeight).build()); addStatusBar(mActivity.mDisplayContent); + mActivity.setVisible(false); mActivity.mDisplayContent.prepareAppTransition(WindowManager.TRANSIT_ACTIVITY_OPEN, false /* alwaysKeepCurrent */); mActivity.mDisplayContent.mOpeningApps.add(mActivity); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java index 786f8d8af024..8c3661b409f4 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java @@ -28,14 +28,17 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.android.server.wm.ActivityStack.ActivityState.RESUMED; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -207,6 +210,40 @@ public class TaskDisplayAreaTests extends WindowTestsBase { false /* reuseCandidate */); } + @Test + public void testGetOrientation_nonResizableHomeStackWithHomeActivityPendingVisibilityChange() { + final RootWindowContainer rootWindowContainer = mWm.mAtmService.mRootWindowContainer; + final TaskDisplayArea defaultTaskDisplayArea = + rootWindowContainer.getDefaultTaskDisplayArea(); + + final ActivityStack rootHomeTask = defaultTaskDisplayArea.getRootHomeTask(); + rootHomeTask.mResizeMode = RESIZE_MODE_UNRESIZEABLE; + + final ActivityStack primarySplitTask = + new ActivityTestsBase.StackBuilder(rootWindowContainer) + .setTaskDisplayArea(defaultTaskDisplayArea) + .setWindowingMode(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY) + .setActivityType(ACTIVITY_TYPE_STANDARD) + .setOnTop(true) + .setCreateActivity(true) + .build(); + ActivityRecord primarySplitActivity = primarySplitTask.getTopNonFinishingActivity(); + assertNotNull(primarySplitActivity); + primarySplitActivity.setState(RESUMED, + "testGetOrientation_nonResizableHomeStackWithHomeActivityPendingVisibilityChange"); + + ActivityRecord homeActivity = rootHomeTask.getTopNonFinishingActivity(); + if (homeActivity == null) { + homeActivity = new ActivityTestsBase.ActivityBuilder(mWm.mAtmService) + .setStack(rootHomeTask).setCreateTask(true).build(); + } + homeActivity.setVisible(false); + homeActivity.mVisibleRequested = true; + assertFalse(rootHomeTask.isVisible()); + + assertEquals(rootWindowContainer.getOrientation(), rootHomeTask.getOrientation()); + } + private void assertGetOrCreateStack(int windowingMode, int activityType, Task candidateTask, boolean reuseCandidate) { final TaskDisplayArea taskDisplayArea = candidateTask.getDisplayArea(); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java index 7ce0c1edfb4c..341e20946c30 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java @@ -729,7 +729,7 @@ public class WindowOrganizerTests extends WindowTestsBase { // We should be rejected from the second sync since we are already // in one. assertEquals(false, bse.addToSyncSet(id2, task)); - finishAndNotifyDrawing(w); + w.immediatelyNotifyBlastSync(); assertEquals(true, bse.addToSyncSet(id2, task)); bse.setReady(id2); } @@ -753,7 +753,7 @@ public class WindowOrganizerTests extends WindowTestsBase { // Since we have a window we have to wait for it to draw to finish sync. verify(transactionListener, never()) .onTransactionReady(anyInt(), any()); - finishAndNotifyDrawing(w); + w.immediatelyNotifyBlastSync(); verify(transactionListener) .onTransactionReady(anyInt(), any()); } @@ -821,14 +821,14 @@ public class WindowOrganizerTests extends WindowTestsBase { int id = bse.startSyncSet(transactionListener); assertEquals(true, bse.addToSyncSet(id, task)); bse.setReady(id); - finishAndNotifyDrawing(w); + w.immediatelyNotifyBlastSync(); // Since we have a child window we still shouldn't be done. verify(transactionListener, never()) .onTransactionReady(anyInt(), any()); reset(transactionListener); - finishAndNotifyDrawing(child); + child.immediatelyNotifyBlastSync(); // Ah finally! Done verify(transactionListener) .onTransactionReady(anyInt(), any()); @@ -1002,20 +1002,15 @@ public class WindowOrganizerTests extends WindowTestsBase { verify(mockCallback, never()).onTransactionReady(anyInt(), any()); assertTrue(w1.useBLASTSync()); assertTrue(w2.useBLASTSync()); - finishAndNotifyDrawing(w1); + w1.immediatelyNotifyBlastSync(); // Even though one Window finished drawing, both windows should still be using blast sync assertTrue(w1.useBLASTSync()); assertTrue(w2.useBLASTSync()); - finishAndNotifyDrawing(w2); + w2.immediatelyNotifyBlastSync(); verify(mockCallback).onTransactionReady(anyInt(), any()); assertFalse(w1.useBLASTSync()); assertFalse(w2.useBLASTSync()); } - - private void finishAndNotifyDrawing(WindowState ws) { - ws.finishDrawing(null); - ws.notifyBlastSyncTransaction(); - } } diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index ee146089b852..8ae1ee99b060 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -38,6 +38,7 @@ import android.annotation.SystemService; import android.annotation.TestApi; import android.annotation.WorkerThread; import android.app.PendingIntent; +import android.app.role.RoleManager; import android.compat.Compatibility; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledAfter; @@ -1885,12 +1886,23 @@ public class TelephonyManager { * Returns the unique device ID, for example, the IMEI for GSM and the MEID * or ESN for CDMA phones. Return null if device ID is not available. * - * <p>Requires Permission: READ_PRIVILEGED_PHONE_STATE, for the calling app to be the device or - * profile owner and have the READ_PHONE_STATE permission, or that the calling app has carrier - * privileges (see {@link #hasCarrierPrivileges}) on any active subscription. The profile owner - * is an app that owns a managed profile on the device; for more details see <a - * href="https://developer.android.com/work/managed-profiles">Work profiles</a>. Profile owner - * access is deprecated and will be removed in a future release. + * <p>Starting with API level 29, persistent device identifiers are guarded behind additional + * restrictions, and apps are recommended to use resettable identifiers (see <a + * href="c"> Best practices for unique identifiers</a>). This method can be invoked if one of + * the following requirements is met: + * <ul> + * <li>If the calling app has been granted the READ_PRIVILEGED_PHONE_STATE permission; this + * is a privileged permission that can only be granted to apps preloaded on the device. + * <li>If the calling app is the device or profile owner and has been granted the + * {@link Manifest.permission#READ_PHONE_STATE} permission. The profile owner is an app that + * owns a managed profile on the device; for more details see <a + * href="https://developer.android.com/work/managed-profiles">Work profiles</a>. + * Profile owner access is deprecated and will be removed in a future release. + * <li>If the calling app has carrier privileges (see {@link #hasCarrierPrivileges}) on any + * active subscription. + * <li>If the calling app is the default SMS role holder (see {@link + * RoleManager#isRoleHeld(String)}). + * </ul> * * <p>If the calling app does not meet one of these requirements then this method will behave * as follows: @@ -1927,12 +1939,23 @@ public class TelephonyManager { * Returns the unique device ID of a subscription, for example, the IMEI for * GSM and the MEID for CDMA phones. Return null if device ID is not available. * - * <p>Requires Permission: READ_PRIVILEGED_PHONE_STATE, for the calling app to be the device or - * profile owner and have the READ_PHONE_STATE permission, or that the calling app has carrier - * privileges (see {@link #hasCarrierPrivileges}) on any active subscription. The profile owner - * is an app that owns a managed profile on the device; for more details see <a - * href="https://developer.android.com/work/managed-profiles">Work profiles</a>. Profile owner - * access is deprecated and will be removed in a future release. + * <p>Starting with API level 29, persistent device identifiers are guarded behind additional + * restrictions, and apps are recommended to use resettable identifiers (see <a + * href="c"> Best practices for unique identifiers</a>). This method can be invoked if one of + * the following requirements is met: + * <ul> + * <li>If the calling app has been granted the READ_PRIVILEGED_PHONE_STATE permission; this + * is a privileged permission that can only be granted to apps preloaded on the device. + * <li>If the calling app is the device or profile owner and has been granted the + * {@link Manifest.permission#READ_PHONE_STATE} permission. The profile owner is an app that + * owns a managed profile on the device; for more details see <a + * href="https://developer.android.com/work/managed-profiles">Work profiles</a>. + * Profile owner access is deprecated and will be removed in a future release. + * <li>If the calling app has carrier privileges (see {@link #hasCarrierPrivileges}) on any + * active subscription. + * <li>If the calling app is the default SMS role holder (see {@link + * RoleManager#isRoleHeld(String)}). + * </ul> * * <p>If the calling app does not meet one of these requirements then this method will behave * as follows: @@ -1985,18 +2008,23 @@ public class TelephonyManager { * Returns the IMEI (International Mobile Equipment Identity). Return null if IMEI is not * available. * - * <p>This API requires one of the following: + * <p>Starting with API level 29, persistent device identifiers are guarded behind additional + * restrictions, and apps are recommended to use resettable identifiers (see <a + * href="c"> Best practices for unique identifiers</a>). This method can be invoked if one of + * the following requirements is met: * <ul> - * <li>The caller holds the READ_PRIVILEGED_PHONE_STATE permission.</li> - * <li>If the caller is the device or profile owner, the caller holds the - * {@link Manifest.permission#READ_PHONE_STATE} permission.</li> - * <li>The caller has carrier privileges (see {@link #hasCarrierPrivileges()} on any - * active subscription.</li> - * <li>The caller is the default SMS app for the device.</li> + * <li>If the calling app has been granted the READ_PRIVILEGED_PHONE_STATE permission; this + * is a privileged permission that can only be granted to apps preloaded on the device. + * <li>If the calling app is the device or profile owner and has been granted the + * {@link Manifest.permission#READ_PHONE_STATE} permission. The profile owner is an app that + * owns a managed profile on the device; for more details see <a + * href="https://developer.android.com/work/managed-profiles">Work profiles</a>. + * Profile owner access is deprecated and will be removed in a future release. + * <li>If the calling app has carrier privileges (see {@link #hasCarrierPrivileges}) on any + * active subscription. + * <li>If the calling app is the default SMS role holder (see {@link + * RoleManager#isRoleHeld(String)}). * </ul> - * <p>The profile owner is an app that owns a managed profile on the device; for more details - * see <a href="https://developer.android.com/work/managed-profiles">Work profiles</a>. - * Access by profile owners is deprecated and will be removed in a future release. * * <p>If the calling app does not meet one of these requirements then this method will behave * as follows: @@ -2058,12 +2086,23 @@ public class TelephonyManager { /** * Returns the MEID (Mobile Equipment Identifier). Return null if MEID is not available. * - * <p>Requires Permission: READ_PRIVILEGED_PHONE_STATE, for the calling app to be the device or - * profile owner and have the READ_PHONE_STATE permission, or that the calling app has carrier - * privileges (see {@link #hasCarrierPrivileges}) on any active subscription. The profile owner - * is an app that owns a managed profile on the device; for more details see <a - * href="https://developer.android.com/work/managed-profiles">Work profiles</a>. Profile owner - * access is deprecated and will be removed in a future release. + * <p>Starting with API level 29, persistent device identifiers are guarded behind additional + * restrictions, and apps are recommended to use resettable identifiers (see <a + * href="c"> Best practices for unique identifiers</a>). This method can be invoked if one of + * the following requirements is met: + * <ul> + * <li>If the calling app has been granted the READ_PRIVILEGED_PHONE_STATE permission; this + * is a privileged permission that can only be granted to apps preloaded on the device. + * <li>If the calling app is the device or profile owner and has been granted the + * {@link Manifest.permission#READ_PHONE_STATE} permission. The profile owner is an app that + * owns a managed profile on the device; for more details see <a + * href="https://developer.android.com/work/managed-profiles">Work profiles</a>. + * Profile owner access is deprecated and will be removed in a future release. + * <li>If the calling app has carrier privileges (see {@link #hasCarrierPrivileges}) on any + * active subscription. + * <li>If the calling app is the default SMS role holder (see {@link + * RoleManager#isRoleHeld(String)}). + * </ul> * * <p>If the calling app does not meet one of these requirements then this method will behave * as follows: @@ -2085,12 +2124,23 @@ public class TelephonyManager { /** * Returns the MEID (Mobile Equipment Identifier). Return null if MEID is not available. * - * <p>Requires Permission: READ_PRIVILEGED_PHONE_STATE, for the calling app to be the device or - * profile owner and have the READ_PHONE_STATE permission, or that the calling app has carrier - * privileges (see {@link #hasCarrierPrivileges}) on any active subscription. The profile owner - * is an app that owns a managed profile on the device; for more details see <a - * href="https://developer.android.com/work/managed-profiles">Work profiles</a>. Profile owner - * access is deprecated and will be removed in a future release. + * <p>Starting with API level 29, persistent device identifiers are guarded behind additional + * restrictions, and apps are recommended to use resettable identifiers (see <a + * href="c"> Best practices for unique identifiers</a>). This method can be invoked if one of + * the following requirements is met: + * <ul> + * <li>If the calling app has been granted the READ_PRIVILEGED_PHONE_STATE permission; this + * is a privileged permission that can only be granted to apps preloaded on the device. + * <li>If the calling app is the device or profile owner and has been granted the + * {@link Manifest.permission#READ_PHONE_STATE} permission. The profile owner is an app that + * owns a managed profile on the device; for more details see <a + * href="https://developer.android.com/work/managed-profiles">Work profiles</a>. + * Profile owner access is deprecated and will be removed in a future release. + * <li>If the calling app has carrier privileges (see {@link #hasCarrierPrivileges}) on any + * active subscription. + * <li>If the calling app is the default SMS role holder (see {@link + * RoleManager#isRoleHeld(String)}). + * </ul> * * <p>If the calling app does not meet one of these requirements then this method will behave * as follows: @@ -2158,12 +2208,25 @@ public class TelephonyManager { /** * Returns the Network Access Identifier (NAI). Return null if NAI is not available. * - * <p>Requires Permission: READ_PRIVILEGED_PHONE_STATE, for the calling app to be the device or - * profile owner and have the READ_PHONE_STATE permission, or that the calling app has carrier - * privileges (see {@link #hasCarrierPrivileges}). The profile owner is an app that owns a - * managed profile on the device; for more details see <a - * href="https://developer.android.com/work/managed-profiles">Work profiles</a>. Profile owner - * access is deprecated and will be removed in a future release. + * <p>Starting with API level 29, persistent device identifiers are guarded behind additional + * restrictions, and apps are recommended to use resettable identifiers (see <a + * href="c"> Best practices for unique identifiers</a>). This method can be invoked if one of + * the following requirements is met: + * <ul> + * <li>If the calling app has been granted the READ_PRIVILEGED_PHONE_STATE permission; this + * is a privileged permission that can only be granted to apps preloaded on the device. + * <li>If the calling app is the device or profile owner and has been granted the + * {@link Manifest.permission#READ_PHONE_STATE} permission. The profile owner is an app that + * owns a managed profile on the device; for more details see <a + * href="https://developer.android.com/work/managed-profiles">Work profiles</a>. + * Profile owner access is deprecated and will be removed in a future release. + * <li>If the calling app has carrier privileges (see {@link #hasCarrierPrivileges}). + * <li>If the calling app is the default SMS role holder (see {@link + * RoleManager#isRoleHeld(String)}). + * </ul> + * + * <p>If the calling app does not meet one of these requirements then this method will behave + * as follows: * * <ul> * <li>If the calling app's target SDK is API level 28 or lower and the app has the @@ -2182,12 +2245,25 @@ public class TelephonyManager { /** * Returns the NAI. Return null if NAI is not available. * - * <p>Requires Permission: READ_PRIVILEGED_PHONE_STATE, for the calling app to be the device or - * profile owner and have the READ_PHONE_STATE permission, or that the calling app has carrier - * privileges (see {@link #hasCarrierPrivileges}). The profile owner is an app that owns a - * managed profile on the device; for more details see <a - * href="https://developer.android.com/work/managed-profiles">Work profiles</a>. Profile owner - * access is deprecated and will be removed in a future release. + * <p>Starting with API level 29, persistent device identifiers are guarded behind additional + * restrictions, and apps are recommended to use resettable identifiers (see <a + * href="c"> Best practices for unique identifiers</a>). This method can be invoked if one of + * the following requirements is met: + * <ul> + * <li>If the calling app has been granted the READ_PRIVILEGED_PHONE_STATE permission; this + * is a privileged permission that can only be granted to apps preloaded on the device. + * <li>If the calling app is the device or profile owner and has been granted the + * {@link Manifest.permission#READ_PHONE_STATE} permission. The profile owner is an app that + * owns a managed profile on the device; for more details see <a + * href="https://developer.android.com/work/managed-profiles">Work profiles</a>. + * Profile owner access is deprecated and will be removed in a future release. + * <li>If the calling app has carrier privileges (see {@link #hasCarrierPrivileges}). + * <li>If the calling app is the default SMS role holder (see {@link + * RoleManager#isRoleHeld(String)}). + * </ul> + * + * <p>If the calling app does not meet one of these requirements then this method will behave + * as follows: * * <ul> * <li>If the calling app's target SDK is API level 28 or lower and the app has the @@ -3775,12 +3851,22 @@ public class TelephonyManager { * Returns the serial number of the SIM, if applicable. Return null if it is * unavailable. * - * <p>Requires Permission: READ_PRIVILEGED_PHONE_STATE, for the calling app to be the device or - * profile owner and have the READ_PHONE_STATE permission, or that the calling app has carrier - * privileges (see {@link #hasCarrierPrivileges}). The profile owner is an app that owns a - * managed profile on the device; for more details see <a - * href="https://developer.android.com/work/managed-profiles">Work profiles</a>. Profile owner - * access is deprecated and will be removed in a future release. + * <p>Starting with API level 29, persistent device identifiers are guarded behind additional + * restrictions, and apps are recommended to use resettable identifiers (see <a + * href="c"> Best practices for unique identifiers</a>). This method can be invoked if one of + * the following requirements is met: + * <ul> + * <li>If the calling app has been granted the READ_PRIVILEGED_PHONE_STATE permission; this + * is a privileged permission that can only be granted to apps preloaded on the device. + * <li>If the calling app is the device or profile owner and has been granted the + * {@link Manifest.permission#READ_PHONE_STATE} permission. The profile owner is an app that + * owns a managed profile on the device; for more details see <a + * href="https://developer.android.com/work/managed-profiles">Work profiles</a>. + * Profile owner access is deprecated and will be removed in a future release. + * <li>If the calling app has carrier privileges (see {@link #hasCarrierPrivileges}). + * <li>If the calling app is the default SMS role holder (see {@link + * RoleManager#isRoleHeld(String)}). + * </ul> * * <p>If the calling app does not meet one of these requirements then this method will behave * as follows: @@ -3803,12 +3889,22 @@ public class TelephonyManager { * Returns the serial number for the given subscription, if applicable. Return null if it is * unavailable. * - * <p>Requires Permission: READ_PRIVILEGED_PHONE_STATE, for the calling app to be the device or - * profile owner and have the READ_PHONE_STATE permission, or that the calling app has carrier - * privileges (see {@link #hasCarrierPrivileges}). The profile owner is an app that owns a - * managed profile on the device; for more details see <a - * href="https://developer.android.com/work/managed-profiles">Work profiles</a>. Profile owner - * access is deprecated and will be removed in a future release. + * <p>Starting with API level 29, persistent device identifiers are guarded behind additional + * restrictions, and apps are recommended to use resettable identifiers (see <a + * href="c"> Best practices for unique identifiers</a>). This method can be invoked if one of + * the following requirements is met: + * <ul> + * <li>If the calling app has been granted the READ_PRIVILEGED_PHONE_STATE permission; this + * is a privileged permission that can only be granted to apps preloaded on the device. + * <li>If the calling app is the device or profile owner and has been granted the + * {@link Manifest.permission#READ_PHONE_STATE} permission. The profile owner is an app that + * owns a managed profile on the device; for more details see <a + * href="https://developer.android.com/work/managed-profiles">Work profiles</a>. + * Profile owner access is deprecated and will be removed in a future release. + * <li>If the calling app has carrier privileges (see {@link #hasCarrierPrivileges}). + * <li>If the calling app is the default SMS role holder (see {@link + * RoleManager#isRoleHeld(String)}). + * </ul> * * <p>If the calling app does not meet one of these requirements then this method will behave * as follows: @@ -4047,12 +4143,22 @@ public class TelephonyManager { * Returns the unique subscriber ID, for example, the IMSI for a GSM phone. * Return null if it is unavailable. * - * <p>Requires Permission: READ_PRIVILEGED_PHONE_STATE, for the calling app to be the device or - * profile owner and have the READ_PHONE_STATE permission, or that the calling app has carrier - * privileges (see {@link #hasCarrierPrivileges}). The profile owner is an app that owns a - * managed profile on the device; for more details see <a - * href="https://developer.android.com/work/managed-profiles">Work profiles</a>. Profile owner - * access is deprecated and will be removed in a future release. + * <p>Starting with API level 29, persistent device identifiers are guarded behind additional + * restrictions, and apps are recommended to use resettable identifiers (see <a + * href="c"> Best practices for unique identifiers</a>). This method can be invoked if one of + * the following requirements is met: + * <ul> + * <li>If the calling app has been granted the READ_PRIVILEGED_PHONE_STATE permission; this + * is a privileged permission that can only be granted to apps preloaded on the device. + * <li>If the calling app is the device or profile owner and has been granted the + * {@link Manifest.permission#READ_PHONE_STATE} permission. The profile owner is an app that + * owns a managed profile on the device; for more details see <a + * href="https://developer.android.com/work/managed-profiles">Work profiles</a>. + * Profile owner access is deprecated and will be removed in a future release. + * <li>If the calling app has carrier privileges (see {@link #hasCarrierPrivileges}). + * <li>If the calling app is the default SMS role holder (see {@link + * RoleManager#isRoleHeld(String)}). + * </ul> * * <p>If the calling app does not meet one of these requirements then this method will behave * as follows: @@ -4076,12 +4182,22 @@ public class TelephonyManager { * for a subscription. * Return null if it is unavailable. * - * <p>Requires Permission: READ_PRIVILEGED_PHONE_STATE, for the calling app to be the device or - * profile owner and have the READ_PHONE_STATE permission, or that the calling app has carrier - * privileges (see {@link #hasCarrierPrivileges}). The profile owner is an app that owns a - * managed profile on the device; for more details see <a - * href="https://developer.android.com/work/managed-profiles">Work profiles</a>. Profile owner - * access is deprecated and will be removed in a future release. + * <p>Starting with API level 29, persistent device identifiers are guarded behind additional + * restrictions, and apps are recommended to use resettable identifiers (see <a + * href="c"> Best practices for unique identifiers</a>). This method can be invoked if one of + * the following requirements is met: + * <ul> + * <li>If the calling app has been granted the READ_PRIVILEGED_PHONE_STATE permission; this + * is a privileged permission that can only be granted to apps preloaded on the device. + * <li>If the calling app is the device or profile owner and has been granted the + * {@link Manifest.permission#READ_PHONE_STATE} permission. The profile owner is an app that + * owns a managed profile on the device; for more details see <a + * href="https://developer.android.com/work/managed-profiles">Work profiles</a>. + * Profile owner access is deprecated and will be removed in a future release. + * <li>If the calling app has carrier privileges (see {@link #hasCarrierPrivileges}). + * <li>If the calling app is the default SMS role holder (see {@link + * RoleManager#isRoleHeld(String)}). + * </ul> * * <p>If the calling app does not meet one of these requirements then this method will behave * as follows: diff --git a/tests/utils/hostutils/src/com/android/internal/util/test/SystemPreparer.java b/tests/utils/hostutils/src/com/android/internal/util/test/SystemPreparer.java index 6bd6985f9675..f30c35aca8da 100644 --- a/tests/utils/hostutils/src/com/android/internal/util/test/SystemPreparer.java +++ b/tests/utils/hostutils/src/com/android/internal/util/test/SystemPreparer.java @@ -356,6 +356,9 @@ public class SystemPreparer extends ExternalResource { /** * Uses shell stop && start to "reboot" the device. May leave invalid state after each test. * Whether this matters or not depends on what's being tested. + * + * TODO(b/159540015): There's a bug with this causing unnecessary disk space usage, which + * can eventually lead to an insufficient storage space error. */ START_STOP } diff --git a/wifi/java/android/net/wifi/SoftApConfiguration.java b/wifi/java/android/net/wifi/SoftApConfiguration.java index 2bcd4f4241a6..a5e76e6c92ee 100644 --- a/wifi/java/android/net/wifi/SoftApConfiguration.java +++ b/wifi/java/android/net/wifi/SoftApConfiguration.java @@ -885,7 +885,8 @@ public final class SoftApConfiguration implements Parcelable { /** * Configure the Soft AP to require manual user control of client association. - * If disabled (the default) then any client can associate to this Soft AP using the + * If disabled (the default) then any client which isn't in the blocked list + * {@link #getBlockedClientList()} can associate to this Soft AP using the * correct credentials until the Soft AP capacity is reached (capacity is hardware, carrier, * or user limited - using {@link #setMaxNumberOfClients(int)}). * @@ -945,21 +946,19 @@ public final class SoftApConfiguration implements Parcelable { } /** - * This method together with {@link setClientControlByUserEnabled(boolean)} control client - * connections to the AP. If client control by user is disabled using the above method then - * this API has no effect and clients are allowed to associate to the AP (within limit of - * max number of clients). + * This API configures the list of clients which are blocked and cannot associate + * to the Soft AP. * - * If client control by user is enabled then this API this API configures the list of - * clients which are blocked. These are rejected. + * <p> + * This method requires hardware support. Hardware support can be determined using + * {@link WifiManager.SoftApCallback#onCapabilityChanged(SoftApCapability)} and + * {@link SoftApCapability#areFeaturesSupported(int)} + * with {@link SoftApCapability.SOFTAP_FEATURE_CLIENT_FORCE_DISCONNECT} * - * All other clients which attempt to associate, whose MAC addresses are on neither list, - * are: - * <ul> - * <li>Rejected</li> - * <li>A callback {@link WifiManager.SoftApCallback#onBlockedClientConnecting(WifiClient)} - * is issued (which allows the user to add them to the allowed client list if desired).<li> - * </ul> + * <p> + * If the method is called on a device without hardware support then starting the soft AP + * using {@link WifiManager#startTetheredHotspot(SoftApConfiguration)} will fail with + * {@link WifiManager#SAP_START_FAILURE_UNSUPPORTED_CONFIGURATION}. * * @param blockedClientList list of clients which are not allowed to associate to the AP. * @return Builder for chaining. diff --git a/wifi/java/android/net/wifi/WifiEnterpriseConfig.java b/wifi/java/android/net/wifi/WifiEnterpriseConfig.java index d35ce3c7a42c..77fa673f1960 100644 --- a/wifi/java/android/net/wifi/WifiEnterpriseConfig.java +++ b/wifi/java/android/net/wifi/WifiEnterpriseConfig.java @@ -93,6 +93,8 @@ public class WifiEnterpriseConfig implements Parcelable { public static final String OPP_KEY_CACHING = "proactive_key_caching"; /** @hide */ public static final String EAP_ERP = "eap_erp"; + /** @hide */ + public static final String OCSP = "ocsp"; /** * String representing the keystore OpenSSL ENGINE's ID. |