Merge "Make MODIFY_QUIET_MODE a development permission." into rvc-dev
diff --git a/apex/blobstore/framework/java/android/app/blob/BlobStoreManager.java b/apex/blobstore/framework/java/android/app/blob/BlobStoreManager.java
index 483d2cc..9c1acaf 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 @@
* @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 @@
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 @@
* 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 @@
* 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 @@
* 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 @@
* 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 @@
* @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 @@
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 4d29045..3d4154a2 100644
--- a/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java
@@ -398,6 +398,26 @@
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()) {
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 265479f..79cd1b1 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 @@
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 @@
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 @@
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 @@
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 a90536fe..f7468d8 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.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.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 @@
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 @@
}
}
+ @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(
@@ -556,7 +606,11 @@
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 +661,10 @@
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 +679,17 @@
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 +720,7 @@
} 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 +734,7 @@
}
// 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());
@@ -957,8 +1020,7 @@
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 +1061,7 @@
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);
}
}
@@ -1076,8 +1137,7 @@
}
if (shouldRemove) {
- blobStoreSession.getSessionFile().delete();
- mActiveBlobIds.remove(blobStoreSession.getSessionId());
+ deleteSessionLocked(blobStoreSession);
deletedBlobIds.add(blobStoreSession.getSessionId());
}
return shouldRemove;
@@ -1089,13 +1149,29 @@
}
@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 +1183,16 @@
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 +1417,11 @@
+ "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 baafff5..2f83be1 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 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.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.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 @@
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 @@
}
}
+ 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 8a3fbde..0c8c9a9 100644
--- a/apex/media/framework/java/android/media/MediaParser.java
+++ b/apex/media/framework/java/android/media/MediaParser.java
@@ -841,6 +841,43 @@
*/
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 @@
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 @@
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 @@
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 @@
*
* <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 @@
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 @@
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 @@
? 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 @@
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 @@
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 @@
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 @@
// 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 @@
// 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 @@
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/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto
index a5f0ac97..ca03343 100644
--- a/cmds/statsd/src/atoms.proto
+++ b/cmds/statsd/src/atoms.proto
@@ -6131,6 +6131,10 @@
*/
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;
}
/**
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index 6f8233d5..c9031b7 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -763,23 +763,24 @@
@Override
public void revokeRuntimePermission(String packageName, String permName, UserHandle user) {
- 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());
- }
- try {
- mPermissionManager
- .revokeRuntimePermission(packageName, permName, user.getIdentifier());
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ revokeRuntimePermission(packageName, permName, user, null);
}
@Override
public void revokeRuntimePermission(String packageName, String permName, UserHandle user,
String reason) {
- // TODO evanseverson: impl
+ if (DEBUG_TRACE_PERMISSION_UPDATES
+ && shouldTraceGrant(packageName, permName, user.getIdentifier())) {
+ Log.i(TAG, "App " + mContext.getPackageName() + " is revoking " + packageName + " "
+ + permName + " for user " + user.getIdentifier() + " with reason " + reason,
+ new RuntimeException());
+ }
+ try {
+ mPermissionManager
+ .revokeRuntimePermission(packageName, permName, user.getIdentifier(), reason);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
@Override
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index a828aac..86a3579 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -1900,13 +1900,11 @@
@Override
public Object getSystemService(String name) {
- // We may override this API from outer context.
- final boolean isUiContext = isUiContext() || getOuterContext().isUiContext();
// Check incorrect Context usage.
- if (isUiComponent(name) && !isUiContext && vmIncorrectContextUseEnabled()) {
+ if (isUiComponent(name) && !isUiContext() && vmIncorrectContextUseEnabled()) {
final String errorMessage = "Tried to access visual service "
+ SystemServiceRegistry.getSystemServiceClassName(name)
- + " from a non-visual Context:" + getOuterContext();
+ + " from a non-visual Context. ";
final String message = "Visual services, such as WindowManager, WallpaperService or "
+ "LayoutInflater should be accessed from Activity or other visual Context. "
+ "Use an Activity or a Context created with "
@@ -2371,7 +2369,6 @@
context.setResources(createResources(mToken, mPackageInfo, mSplitName, displayId,
overrideConfiguration, getDisplayAdjustments(displayId).getCompatibilityInfo(),
mResources.getLoaders()));
- context.mIsUiContext = isUiContext() || getOuterContext().isUiContext();
return context;
}
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index e84c5e5..9459577 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -677,4 +677,10 @@
* 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 82e9881..ce51dba 100644
--- a/core/java/android/app/UiAutomationConnection.java
+++ b/core/java/android/app/UiAutomationConnection.java
@@ -294,7 +294,7 @@
}
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 30ee326..15625cd 100644
--- a/core/java/android/hardware/camera2/CameraDevice.java
+++ b/core/java/android/hardware/camera2/CameraDevice.java
@@ -680,7 +680,7 @@
* </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 @@
* </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 @@
* 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 20d9c30..776d155 100644
--- a/core/java/android/hardware/camera2/params/MandatoryStreamCombination.java
+++ b/core/java/android/hardware/camera2/params/MandatoryStreamCombination.java
@@ -685,6 +685,12 @@
"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 @@
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 @@
case s1440p:
formatSize = size1440p;
break;
+ case VGA:
+ formatSize = sizeVGAp;
+ break;
default:
formatSize = size720p;
}
diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java
index ef2a8a1..b36aeb8 100755
--- a/core/java/android/os/Build.java
+++ b/core/java/android/os/Build.java
@@ -133,12 +133,23 @@
* <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 @@
* 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 235b083..e231021 100644
--- a/core/java/android/permission/IPermissionManager.aidl
+++ b/core/java/android/permission/IPermissionManager.aidl
@@ -71,7 +71,7 @@
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/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index 9f7c5e4..daeb1c9 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -229,7 +229,7 @@
int transformHint);
@Nullable
- @GuardedBy("sLock")
+ @GuardedBy("mLock")
private ArrayList<OnReparentListener> mReparentListeners;
/**
diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java
index 5b6e5c1..0d21eb5 100644
--- a/core/java/android/view/SurfaceView.java
+++ b/core/java/android/view/SurfaceView.java
@@ -1231,7 +1231,7 @@
float postScaleX, float postScaleY) {
transaction.setPosition(surface, positionLeft, positionTop);
transaction.setMatrix(surface, postScaleX /*dsdx*/, 0f /*dtdx*/,
- 0f /*dsdy*/, postScaleY /*dtdy*/);
+ 0f /*dtdy*/, postScaleY /*dsdy*/);
}
/** @hide */
diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java
index ffeeb80..0d2d4d1 100644
--- a/core/java/android/view/ViewConfiguration.java
+++ b/core/java/android/view/ViewConfiguration.java
@@ -500,13 +500,12 @@
*/
public static ViewConfiguration get(Context context) {
if (!context.isUiContext() && vmIncorrectContextUseEnabled()) {
- final String errorMessage = "Tried to access UI constants from a non-visual Context:"
- + context;
+ final String errorMessage = "Tried to access UI constants from a non-visual Context.";
final String message = "UI constants, such as display metrics or window metrics, "
+ "must be accessed from Activity or other visual Context. "
+ "Use an Activity or a Context created with "
+ "Context#createWindowContext(int, Bundle), which are adjusted to the "
- + "configuration and visual bounds of an area on screen";
+ + "configuration and visual bounds of an area on screen.";
final Exception exception = new IllegalArgumentException(errorMessage);
StrictMode.onIncorrectContextUsed(message, exception);
Log.e(TAG, errorMessage + message, exception);
diff --git a/core/java/android/widget/inline/InlineContentView.java b/core/java/android/widget/inline/InlineContentView.java
index be7c696..9712311 100644
--- a/core/java/android/widget/inline/InlineContentView.java
+++ b/core/java/android/widget/inline/InlineContentView.java
@@ -21,8 +21,8 @@
import android.annotation.TestApi;
import android.content.Context;
import android.graphics.PixelFormat;
-import android.graphics.Rect;
import android.graphics.PointF;
+import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.SurfaceControl;
diff --git a/core/java/com/android/internal/app/PlatLogoActivity.java b/core/java/com/android/internal/app/PlatLogoActivity.java
index 2a7eae6..986bbc8 100644
--- a/core/java/com/android/internal/app/PlatLogoActivity.java
+++ b/core/java/com/android/internal/app/PlatLogoActivity.java
@@ -55,6 +55,10 @@
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 @@
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 @@
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 @@
}
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 @@
if (isLocked() && oldUserLevel != STEPS - 1 && getUserLevel() == STEPS - 1) {
mUnlockTries--;
+ } else if (!isLocked() && getUserLevel() == 0) {
+ mUnlockTries = UNLOCK_TRIES;
}
if (!isLocked()) {
diff --git a/core/res/res/layout/notification_template_material_conversation.xml b/core/res/res/layout/notification_template_material_conversation.xml
index 82e99e6..861a056 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,11 +138,11 @@
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"
- android:layout_width="0dp"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/notification_conversation_header_separating_margin"
android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title"
diff --git a/identity/java/android/security/identity/IdentityCredential.java b/identity/java/android/security/identity/IdentityCredential.java
index 493c85a..4eb6e42 100644
--- a/identity/java/android/security/identity/IdentityCredential.java
+++ b/identity/java/android/security/identity/IdentityCredential.java
@@ -41,19 +41,18 @@
/**
* 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 @@
/**
* 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 @@
/**
* 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 @@
*
* <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 @@
* 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 37de2c4..71860d2 100644
--- a/identity/java/android/security/identity/ResultData.java
+++ b/identity/java/android/security/identity/ResultData.java
@@ -68,8 +68,8 @@
* {@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 @@
* ]
*
* 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 @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/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java
index 62d76c0..0780c68 100644
--- a/media/java/android/media/MediaCodec.java
+++ b/media/java/android/media/MediaCodec.java
@@ -2762,7 +2762,7 @@
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/jni/android_media_tv_Tuner.cpp b/media/jni/android_media_tv_Tuner.cpp
index e8f18a5..515d610 100644
--- a/media/jni/android_media_tv_Tuner.cpp
+++ b/media/jni/android_media_tv_Tuner.cpp
@@ -335,7 +335,7 @@
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/packages/EasterEgg/Android.bp b/packages/EasterEgg/Android.bp
index 43ed810..b858ab0 100644
--- a/packages/EasterEgg/Android.bp
+++ b/packages/EasterEgg/Android.bp
@@ -23,11 +23,23 @@
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 7f76a45..57c459b 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" />
-
- <!-- <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>
+
+ <!-- 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 0000000..20b4698
--- /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 0000000..e8e6450
--- /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 0000000..73fd37f
--- /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 0000000..b55d65c
--- /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 0000000..8b0e9af
--- /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 0000000..8608720
--- /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 0000000..33fa921
--- /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 0000000..d8b4cc5
--- /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 0000000..5e4d0fd
--- /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 0000000..a89fb4f
--- /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 0000000..1b2bb6f
--- /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 0000000..95b2fb5
--- /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 0000000..74dd134
--- /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 0000000..3fff6a9
--- /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 0000000..eaf831e
--- /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 0000000..28cf4a2
--- /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 0000000..0d90859
--- /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 0000000..364ba0c
--- /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 0000000..e3a512a
--- /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 0000000..66b78fa
--- /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 0000000..df600a8
--- /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 0000000..d55565d9
--- /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 0000000..60ea36b
--- /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 0000000..54961af
--- /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 0000000..5dca3d1
--- /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 0000000..8cebc7e
--- /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 0000000..411084b
--- /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 0000000..bb01e9f
--- /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 0000000..8fe84ff
--- /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 0000000..ba3dc33
--- /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 0000000..7d94b24
--- /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 0000000..eed171d
--- /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 0000000..28b1fa8
--- /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 0000000..7f8d4fa
--- /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 659f98b..31b2a7f 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 0000000..2b98736
--- /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 0000000..1d947ed
--- /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 0000000..4dde1b6
--- /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 0000000..d72c746
--- /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 0000000..a772a87
--- /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 0000000..b01bd69
--- /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 0000000..d471236
--- /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 0000000..e5868eb
--- /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 0000000..ddcf2e8
--- /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 0000000..d403cd1
--- /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 0000000..1e46cf4
--- /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 0000000..b9fb4d1
--- /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 0000000..86b6e34
--- /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 0000000..a1871a6
--- /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 0000000..608cb20
--- /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 0000000..0cca23c
--- /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 0000000..b82f6f9
--- /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 0000000..bb1ff12
--- /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 a4c17af..8e916b0 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 0000000..85b494d
--- /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 0000000..9f7ac802
--- /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 0000000..d0ca0c8
--- /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 0000000..c258137
--- /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 0000000..5214fc1
--- /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 0000000..e9dcebd
--- /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 b95ec6b..25f9421 100644
--- a/packages/EasterEgg/res/values/strings.xml
+++ b/packages/EasterEgg/res/values/strings.xml
@@ -14,11 +14,13 @@
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 0000000..2130025
--- /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 0000000..cd59a73
--- /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 0000000..aeffc4a
--- /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 0000000..df461c6
--- /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 0000000..56f599a
--- /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 0000000..2bd2228
--- /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 0000000..8ed8087
--- /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 0000000..ca89adc7
--- /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 0000000..939e85c
--- /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 0000000..d02433f
--- /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 0000000..49ff315
--- /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/SystemUI/res/layout/controls_management.xml b/packages/SystemUI/res/layout/controls_management.xml
index 46f79de..ae7f44d 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 0e3fa1e..db45a60 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 b615885..6dc8322 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
@@ -137,6 +137,7 @@
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;
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index ccfbd8f..b739999 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 @@
/** 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;
@@ -1291,6 +1297,7 @@
// Collapsing? Do this first before remaining steps.
if (update.expandedChanged && !update.expanded) {
mStackView.setExpanded(false);
+ mNotificationShadeWindowController.setForceHasTopUi(mHadTopUi);
}
// Do removals, if any.
@@ -1377,6 +1384,8 @@
if (update.expandedChanged && update.expanded) {
if (mStackView != null) {
mStackView.setExpanded(true);
+ mHadTopUi = mNotificationShadeWindowController.getForceHasTopUi();
+ mNotificationShadeWindowController.setForceHasTopUi(true);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
index 7020f1c..c170ee2 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 @@
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/BubbleExperimentConfig.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java
index a888bd5..ffb650d6 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 @@
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 @@
}
/**
- * 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 a389e2b..09e8799 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -1152,9 +1152,6 @@
}
private void setUpOverflow() {
- if (!BubbleExperimentConfig.allowBubbleOverflow(mContext)) {
- return;
- }
int overflowBtnIndex = 0;
if (mBubbleOverflow == null) {
mBubbleOverflow = new BubbleOverflow(getContext());
@@ -1520,8 +1517,7 @@
}
private void updateOverflowVisibility() {
- if (!BubbleExperimentConfig.allowBubbleOverflow(mContext)
- || mBubbleOverflow == null) {
+ if (mBubbleOverflow == null) {
return;
}
mBubbleOverflow.setVisible(mIsExpanded ? VISIBLE : GONE);
@@ -2778,11 +2774,8 @@
* @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/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
index e3fbdbc..468b9b1 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 @@
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/util/sensors/ProximitySensor.java b/packages/SystemUI/src/com/android/systemui/util/sensors/ProximitySensor.java
index 6794a2a..52d4647 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 @@
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/services/accessibility/java/com/android/server/accessibility/FullScreenMagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/FullScreenMagnificationGestureHandler.java
index afe6238..b7f8e67 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 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.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 @@
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 @@
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 @@
transitionToDelegatingStateAndClear();
}
break;
+ case MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE: {
+ transitToPanningScalingStateAndClear();
+ }
+ break;
default: {
throw new IllegalArgumentException("Unknown message type: " + type);
}
@@ -702,14 +710,20 @@
}
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 @@
// 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 @@
}
}
+ 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 @@
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 @@
transitionTo(mDelegatingState);
sendDelayedMotionEvents();
removePendingDelayedMessages();
+ mSecondPointerDownLocation.set(Float.NaN, Float.NaN);
}
private void onTripleTap(MotionEvent up) {
@@ -907,6 +947,10 @@
}
}
+ 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/GestureUtils.java b/services/accessibility/java/com/android/server/accessibility/gestures/GestureUtils.java
index ac67480..ec30418 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 @@
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/core/Android.bp b/services/core/Android.bp
index 65e98ac..4bba0d8 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -95,7 +95,6 @@
"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 @@
"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/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index be85906..e77b361 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.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 @@
*/
@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);
@@ -9202,16 +9194,31 @@
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);
@@ -14707,7 +14714,6 @@
mProcessesToGc.remove(app);
mPendingPssProcesses.remove(app);
- mTopUiOrRunningRemoteAnimApps.remove(app);
ProcessList.abortNextPssTime(app.procStateMemTracker);
// Dismiss any open dialogs.
@@ -18508,22 +18514,6 @@
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 58b0a15..da5f489 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 @@
// 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 @@
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/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java
index 4c75ab2..c5152c0 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 @@
void setHasTopUi(boolean hasTopUi) {
mHasTopUi = hasTopUi;
mWindowProcessController.setHasTopUi(hasTopUi);
- updateTopUiOrRunningRemoteAnim();
}
boolean hasTopUi() {
@@ -1519,19 +1518,10 @@
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 f8ab6f4..5e908b2 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 @@
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 @@
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 @@
}
// 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 void applyAllVolumes() {
- final boolean isAvrcpAbsVolSupported = mDeviceBroker.isAvrcpAbsoluteVolumeSupported();
synchronized (VolumeStreamState.class) {
// apply device specific volumes first
int index;
@@ -5715,7 +5717,7 @@
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 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 @@
|| (isFixedVolumeDevice(device) && index != 0)) {
mIndexMap.put(device, mIndexMax);
}
- applyDeviceVolume_syncVSS(device, isAvrcpAbsVolSupported);
+ applyDeviceVolume_syncVSS(device);
}
}
}
@@ -6130,11 +6131,9 @@
/*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 @@
// 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 @@
// 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 @@
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 b84d322..75ab33d 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 @@
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 @@
// 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 @@
* 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 @@
HdmiPortInfo[] getPortInfos() {
- return mNativeWrapperImpl.nativeGetPortInfos(mNativePtr);
+ return mNativeWrapperImpl.nativeGetPortInfos();
}
/**
@@ -289,7 +295,7 @@
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 @@
for (int i = 0; i < mLocalDevices.size(); ++i) {
mLocalDevices.valueAt(i).clearAddress();
}
- mNativeWrapperImpl.nativeClearLogicalAddress(mNativePtr);
+ mNativeWrapperImpl.nativeClearLogicalAddress();
}
@ServiceThreadOnly
@@ -326,7 +332,7 @@
@ServiceThreadOnly
int getPhysicalAddress() {
assertRunOnServiceThread();
- return mNativeWrapperImpl.nativeGetPhysicalAddress(mNativePtr);
+ return mNativeWrapperImpl.nativeGetPhysicalAddress();
}
/**
@@ -337,7 +343,7 @@
@ServiceThreadOnly
int getVersion() {
assertRunOnServiceThread();
- return mNativeWrapperImpl.nativeGetVersion(mNativePtr);
+ return mNativeWrapperImpl.nativeGetVersion();
}
/**
@@ -348,7 +354,7 @@
@ServiceThreadOnly
int getVendorId() {
assertRunOnServiceThread();
- return mNativeWrapperImpl.nativeGetVendorId(mNativePtr);
+ return mNativeWrapperImpl.nativeGetVendorId();
}
/**
@@ -361,7 +367,7 @@
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 @@
if (!LanguageTag.isLanguage(language)) {
return;
}
- mNativeWrapperImpl.nativeSetLanguage(mNativePtr, language);
+ mNativeWrapperImpl.nativeSetLanguage(language);
}
/**
@@ -387,7 +393,7 @@
@ServiceThreadOnly
void enableAudioReturnChannel(int port, boolean enabled) {
assertRunOnServiceThread();
- mNativeWrapperImpl.nativeEnableAudioReturnChannel(mNativePtr, port, enabled);
+ mNativeWrapperImpl.nativeEnableAudioReturnChannel(port, enabled);
}
/**
@@ -399,7 +405,7 @@
@ServiceThreadOnly
boolean isConnected(int port) {
assertRunOnServiceThread();
- return mNativeWrapperImpl.nativeIsConnected(mNativePtr, port);
+ return mNativeWrapperImpl.nativeIsConnected(port);
}
/**
@@ -521,7 +527,7 @@
// <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 @@
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 @@
}
/**
- * 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 @@
}
/**
- * 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 @@
}
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 @@
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 long nativeInit(HdmiCecController handler, MessageQueue messageQueue) {
- return HdmiCecController.nativeInit(handler, messageQueue);
+ 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 int nativeSendCecCommand(long controllerPtr, int srcAddress, int dstAddress,
- byte[] body) {
- return HdmiCecController.nativeSendCecCommand(controllerPtr, srcAddress, dstAddress, body);
+ public void setCallback(HdmiCecCallback callback) {
+ try {
+ mHdmiCec.setCallback(callback);
+ } catch (RemoteException e) {
+ HdmiLogger.error("Couldn't initialise tv.cec callback : ", e);
+ }
}
@Override
- public int nativeAddLogicalAddress(long controllerPtr, int logicalAddress) {
- return HdmiCecController.nativeAddLogicalAddress(controllerPtr, logicalAddress);
+ 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 void nativeClearLogicalAddress(long controllerPtr) {
- HdmiCecController.nativeClearLogicalAddress(controllerPtr);
+ 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 int nativeGetPhysicalAddress(long controllerPtr) {
- return HdmiCecController.nativeGetPhysicalAddress(controllerPtr);
+ public void nativeClearLogicalAddress() {
+ try {
+ mHdmiCec.clearLogicalAddress();
+ } catch (RemoteException e) {
+ HdmiLogger.error("Failed to clear logical address : ", e);
+ }
}
@Override
- public int nativeGetVersion(long controllerPtr) {
- return HdmiCecController.nativeGetVersion(controllerPtr);
+ 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 nativeGetVendorId(long controllerPtr) {
- return HdmiCecController.nativeGetVendorId(controllerPtr);
+ public int nativeGetVersion() {
+ try {
+ return mHdmiCec.getCecVersion();
+ } catch (RemoteException e) {
+ HdmiLogger.error("Failed to get cec version : ", e);
+ return Result.FAILURE_UNKNOWN;
+ }
}
@Override
- public HdmiPortInfo[] nativeGetPortInfos(long controllerPtr) {
- return HdmiCecController.nativeGetPortInfos(controllerPtr);
+ 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 nativeSetOption(long controllerPtr, int flag, boolean enabled) {
- HdmiCecController.nativeSetOption(controllerPtr, flag, enabled);
+ 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 void nativeSetLanguage(long controllerPtr, String language) {
- HdmiCecController.nativeSetLanguage(controllerPtr, language);
+ public void nativeSetOption(int flag, boolean enabled) {
+ try {
+ mHdmiCec.setOption(flag, enabled);
+ } catch (RemoteException e) {
+ HdmiLogger.error("Failed to set option : ", e);
+ }
}
@Override
- public void nativeEnableAudioReturnChannel(long controllerPtr, int port, boolean flag) {
- HdmiCecController.nativeEnableAudioReturnChannel(controllerPtr, port, flag);
+ public void nativeSetLanguage(String language) {
+ try {
+ mHdmiCec.setLanguage(language);
+ } catch (RemoteException e) {
+ HdmiLogger.error("Failed to set language : ", e);
+ }
}
@Override
- public boolean nativeIsConnected(long controllerPtr, int port) {
- return HdmiCecController.nativeIsConnected(controllerPtr, port);
+ 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 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 serviceDied(long cookie) {
+ if (cookie == HDMI_CEC_HAL_DEATH_COOKIE) {
+ HdmiLogger.error(TAG, "Service died cokkie : " + cookie + "; reconnecting");
+ connectToHal();
+ }
+ }
+
+ @Override
+ 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 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 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 2309293..8da3c93 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 @@
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 @@
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 d8acf0e..85544d0 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 @@
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 @@
setProperties(PROPERTIES);
setAllowed(true);
+
+ sendMessage(INITIALIZE_HANDLER, 0, null);
}
/**
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index 242132c..b45d450 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.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 @@
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 @@
instantiateCustomProvider(null);
instantiateCustomDispatcher(null);
+ mRecordThread.start();
}
private boolean isGlobalPriorityActiveLocked() {
@@ -599,8 +602,8 @@
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 @@
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/pm/AppsFilter.java b/services/core/java/com/android/server/pm/AppsFilter.java
index 8a7702e..3a203d5 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 @@
@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 @@
@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 @@
* 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 @@
}
}
// 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 @@
}
/**
+ * 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 @@
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 @@
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 @@
}
}
});
- mForceQueryable.remove(setting.appId);
-
-
}
/**
@@ -1091,16 +1111,14 @@
}
/** 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 de8ad6b..994cec2 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 @@
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 @@
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 @@
*
* <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 @@
return;
}
}
- List<PackageInstallerSession> childSessions = getChildSessions();
+ List<PackageInstallerSession> childSessions = getChildSessionsNotLocked();
synchronized (mLock) {
try {
sealLocked(childSessions);
@@ -1649,7 +1654,7 @@
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 @@
// 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 @@
"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 @@
mCallback.onStagedSessionChanged(this);
return;
}
- cleanStageDir();
+ cleanStageDir(childSessions);
}
if (mRelinquished) {
@@ -3055,7 +3062,7 @@
mStagedSessionErrorMessage = errorMessage;
Slog.d(TAG, "Marking session " + sessionId + " as failed: " + errorMessage);
}
- cleanStageDir();
+ cleanStageDirNotLocked();
mCallback.onStagedSessionChanged(this);
}
@@ -3070,7 +3077,7 @@
mStagedSessionErrorMessage = "";
Slog.d(TAG, "Marking session " + sessionId + " as applied");
}
- cleanStageDir();
+ cleanStageDirNotLocked();
mCallback.onStagedSessionChanged(this);
}
@@ -3128,20 +3135,37 @@
}
}
- 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 dc7ed34..a9d3a07 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -12368,7 +12368,9 @@
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 0dc4d13..1a7490e8 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 @@
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/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
index 1b11e2d..4f0b689 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_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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
}
@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 @@
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 @@
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 @@
} 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 @@
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 @@
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 @@
}
}
- 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 @@
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;
- }
- } 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_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;
+ if (permissionsState.hasPermission(permissionName, userId)) {
+ if (oldGrantedRestrictedPermissions.get(userId) == null) {
+ oldGrantedRestrictedPermissions.put(userId, new ArraySet<>());
+ }
+ oldGrantedRestrictedPermissions.get(userId).add(permissionName);
}
- }
- if (oldFlags == newFlags) {
- continue;
- }
+ final int oldFlags = permissionsState.getPermissionFlags(permissionName, userId);
- 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;
+ 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 |= 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 |= 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;
+ }
}
- }
- // 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 (oldFlags == newFlags) {
+ continue;
+ }
- updatePermissionFlagsInternal(permissionName, pkg.getPackageName(), mask, newFlags,
- callingUid, userId, false, null /*callback*/);
+ 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 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*/);
+ }
}
if (updatePermissions) {
@@ -3861,15 +3877,22 @@
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 @@
overridePolicy,
Process.SYSTEM_UID,
userId,
- callback);
+ null, callback);
} catch (IllegalArgumentException e) {
Slog.e(TAG,
"Failed to revoke "
@@ -4624,10 +4647,8 @@
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 4412162..2e83b23 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 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/soundtrigger_middleware/README.md b/services/core/java/com/android/server/soundtrigger_middleware/README.md
new file mode 100644
index 0000000..416548d
--- /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 f4c77a0..5d25d2c 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.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 @@
}
/** 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 @@
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 @@
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 @@
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 @@
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 @@
// 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 @@
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 @@
@Override
public void onRecognition(int modelHandle, @NonNull RecognitionEvent event) {
- synchronized (SoundTriggerMiddlewareValidation.this) {
- if (event.status != RecognitionStatus.FORCED) {
- mLoadedModels.get(modelHandle).activityState =
- ModelState.Activity.LOADED;
+ // 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);
- }
+ }
+ 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;
+ // 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);
- }
+ }
+ 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 @@
// 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 522e5e1..f809ed4 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 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,18 +154,28 @@
*
* @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 @@
@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 @@
/**
* 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 @@
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 @@
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 @@
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 @@
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 09fd33d..dbdef23 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 @@
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/uri/UriGrantsManagerService.java b/services/core/java/com/android/server/uri/UriGrantsManagerService.java
index c38d649..5f63233 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 @@
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/ActivityStartController.java b/services/core/java/com/android/server/wm/ActivityStartController.java
index 6fbfa68..16ca60d 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.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 @@
// 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 @@
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 @@
? 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 daa97b5..25842f5 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 @@
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);
}
@@ -2632,6 +2638,11 @@
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 cf453c7..205523b 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 @@
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 @@
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/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 9b5d94e..afe40b8 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 @@
}
activity.onRemovedFromDisplay();
+ if (activity == mFixedRotationLaunchingApp) {
+ setFixedRotationLaunchingAppUnchecked(null);
+ }
}
@Override
@@ -1468,6 +1471,12 @@
// 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
@@ -5636,6 +5645,12 @@
*/
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/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index 7b69038..e230807 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 @@
// 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 52fb941..564eecf 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 @@
}
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 fbc5afa..46e1bf0 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 @@
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 26a1fea..49ef4e4 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 @@
void removeIfPossible() {
super.removeIfPossible();
removeIfPossible(false /*keepVisibleDeadWindow*/);
- finishDrawing(null);
+ immediatelyNotifyBlastSync();
}
private void removeIfPossible(boolean keepVisibleDeadWindow) {
@@ -5806,7 +5806,7 @@
// 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 +5844,6 @@
return mWinAnimator.finishDrawingLocked(postDrawTransaction);
}
- mWmService.mH.removeMessages(WINDOW_STATE_BLAST_SYNC_TIMEOUT, this);
if (postDrawTransaction != null) {
mBLASTSyncTransaction.merge(postDrawTransaction);
}
@@ -5853,8 +5852,9 @@
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 +5877,11 @@
mNotifyBlastOnSurfacePlacement = false;
}
+ void immediatelyNotifyBlastSync() {
+ finishDrawing(null);
+ notifyBlastSyncTransaction();
+ }
+
private boolean requestResizeForBlastSync() {
return useBLASTSync() && !mResizeForBlastSyncReported;
}
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 cdafd32..fde40aa 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 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 @@
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 @@
@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/blob/BlobStoreManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java
index 7446289..4e2f9a4 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 @@
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 @@
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 @@
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 @@
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();
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 2007d4f..1cbee12 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_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 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.Test;
import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
import java.util.function.IntConsumer;
/**
@@ -106,6 +112,7 @@
// 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 @@
});
}
+ @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 @@
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 @@
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;
+ private MotionEvent pointerEvent(int action, float x, float 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 8607ec6..7538468 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 @@
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 @@
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 @@
}
@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 @@
}
@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/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 2171d75..668f047 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 @@
.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 4dbf79a..1d13788 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 @@
@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 6896740..105af4f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -1081,6 +1081,7 @@
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 +1136,7 @@
// 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 +1279,14 @@
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 130e555..3c98272 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 @@
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 786f8d8..8c3661b 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_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 @@
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 7ce0c1e..341e209 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 @@
// 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 @@
// 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 @@
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 @@
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 ee14608..8ae1ee9 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -38,6 +38,7 @@
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 @@
* 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 @@
* 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 @@
* 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 @@
/**
* 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 @@
/**
* 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 @@
/**
* 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 @@
/**
* 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 @@
* 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 @@
* 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 @@
* 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 @@
* 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/wifi/java/android/net/wifi/SoftApConfiguration.java b/wifi/java/android/net/wifi/SoftApConfiguration.java
index 2bcd4f4..a5e76e6 100644
--- a/wifi/java/android/net/wifi/SoftApConfiguration.java
+++ b/wifi/java/android/net/wifi/SoftApConfiguration.java
@@ -885,7 +885,8 @@
/**
* 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 @@
}
/**
- * 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 d35ce3c..77fa673 100644
--- a/wifi/java/android/net/wifi/WifiEnterpriseConfig.java
+++ b/wifi/java/android/net/wifi/WifiEnterpriseConfig.java
@@ -93,6 +93,8 @@
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.