summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java20
-rw-r--r--apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java32
-rw-r--r--apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java5
-rw-r--r--apex/media/framework/java/android/media/MediaParser.java17
-rw-r--r--apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/libstats/LibStatsPullTests.java1
-rw-r--r--apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/libstats/StatsConfigUtils.java (renamed from apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/StatsConfigUtils.java)2
-rw-r--r--cmds/statsd/src/atoms.proto1
-rw-r--r--core/java/android/app/ApplicationPackageManager.java17
-rw-r--r--core/java/android/app/ContextImpl.java7
-rw-r--r--core/java/android/app/IActivityManager.aidl6
-rw-r--r--core/java/android/app/UiAutomationConnection.java2
-rw-r--r--core/java/android/permission/IPermissionManager.aidl2
-rw-r--r--core/java/android/view/InsetsState.java3
-rw-r--r--core/java/android/view/ViewConfiguration.java5
-rw-r--r--core/java/android/widget/inline/InlineContentView.java5
-rw-r--r--core/java/com/android/internal/app/ChooserListAdapter.java2
-rw-r--r--core/java/com/android/internal/app/PlatLogoActivity.java34
-rw-r--r--core/java/com/android/internal/app/ResolverActivity.java39
-rw-r--r--core/res/AndroidManifest.xml2
-rw-r--r--core/res/res/layout/notification_template_material_conversation.xml7
-rw-r--r--core/tests/coretests/src/android/view/InsetsStateTest.java1
-rw-r--r--identity/java/android/security/identity/IdentityCredential.java29
-rw-r--r--identity/java/android/security/identity/ResultData.java16
-rw-r--r--packages/CarSystemUI/res/layout/notification_center_activity.xml16
-rw-r--r--packages/EasterEgg/Android.bp18
-rw-r--r--packages/EasterEgg/AndroidManifest.xml92
-rw-r--r--packages/EasterEgg/build.gradle82
-rw-r--r--packages/EasterEgg/gradle.properties23
-rw-r--r--packages/EasterEgg/res/drawable/android_11_dial.xml63
-rw-r--r--packages/EasterEgg/res/drawable/back.xml22
-rw-r--r--packages/EasterEgg/res/drawable/belly.xml22
-rw-r--r--packages/EasterEgg/res/drawable/body.xml22
-rw-r--r--packages/EasterEgg/res/drawable/bowtie.xml22
-rw-r--r--packages/EasterEgg/res/drawable/cap.xml22
-rw-r--r--packages/EasterEgg/res/drawable/collar.xml22
-rw-r--r--packages/EasterEgg/res/drawable/face_spot.xml22
-rw-r--r--packages/EasterEgg/res/drawable/food_bits.xml33
-rw-r--r--packages/EasterEgg/res/drawable/food_chicken.xml39
-rw-r--r--packages/EasterEgg/res/drawable/food_cookie.xml35
-rw-r--r--packages/EasterEgg/res/drawable/food_dish.xml24
-rw-r--r--packages/EasterEgg/res/drawable/food_donut.xml24
-rw-r--r--packages/EasterEgg/res/drawable/food_sysuituna.xml24
-rw-r--r--packages/EasterEgg/res/drawable/foot1.xml22
-rw-r--r--packages/EasterEgg/res/drawable/foot2.xml22
-rw-r--r--packages/EasterEgg/res/drawable/foot3.xml22
-rw-r--r--packages/EasterEgg/res/drawable/foot4.xml22
-rw-r--r--packages/EasterEgg/res/drawable/head.xml22
-rw-r--r--packages/EasterEgg/res/drawable/ic_bowl.xml34
-rw-r--r--packages/EasterEgg/res/drawable/ic_close.xml24
-rw-r--r--packages/EasterEgg/res/drawable/ic_foodbowl_filled.xml44
-rw-r--r--packages/EasterEgg/res/drawable/ic_fullcat_icon.xml108
-rw-r--r--packages/EasterEgg/res/drawable/ic_share.xml24
-rw-r--r--packages/EasterEgg/res/drawable/ic_toy_ball.xml29
-rw-r--r--packages/EasterEgg/res/drawable/ic_toy_fish.xml41
-rw-r--r--packages/EasterEgg/res/drawable/ic_toy_laser.xml42
-rw-r--r--packages/EasterEgg/res/drawable/ic_toy_mouse.xml45
-rw-r--r--packages/EasterEgg/res/drawable/ic_water.xml27
-rw-r--r--packages/EasterEgg/res/drawable/ic_water_filled.xml24
-rw-r--r--packages/EasterEgg/res/drawable/ic_waterbowl_filled.xml33
-rw-r--r--packages/EasterEgg/res/drawable/icon.xml19
-rw-r--r--packages/EasterEgg/res/drawable/icon_bg.xml8
-rw-r--r--packages/EasterEgg/res/drawable/left_ear.xml22
-rw-r--r--packages/EasterEgg/res/drawable/left_ear_inside.xml22
-rw-r--r--packages/EasterEgg/res/drawable/left_eye.xml22
-rw-r--r--packages/EasterEgg/res/drawable/leg1.xml22
-rw-r--r--packages/EasterEgg/res/drawable/leg2.xml22
-rw-r--r--packages/EasterEgg/res/drawable/leg2_shadow.xml22
-rw-r--r--packages/EasterEgg/res/drawable/leg3.xml22
-rw-r--r--packages/EasterEgg/res/drawable/leg4.xml22
-rw-r--r--packages/EasterEgg/res/drawable/mouth.xml27
-rw-r--r--packages/EasterEgg/res/drawable/nose.xml22
-rw-r--r--packages/EasterEgg/res/drawable/octo_bg.xml8
-rw-r--r--packages/EasterEgg/res/drawable/right_ear.xml22
-rw-r--r--packages/EasterEgg/res/drawable/right_ear_inside.xml23
-rw-r--r--packages/EasterEgg/res/drawable/right_eye.xml22
-rw-r--r--packages/EasterEgg/res/drawable/stat_icon.xml30
-rw-r--r--packages/EasterEgg/res/drawable/tail.xml26
-rw-r--r--packages/EasterEgg/res/drawable/tail_cap.xml22
-rw-r--r--packages/EasterEgg/res/drawable/tail_shadow.xml22
-rw-r--r--packages/EasterEgg/res/layout/activity_paint.xml4
-rw-r--r--packages/EasterEgg/res/layout/cat_view.xml82
-rw-r--r--packages/EasterEgg/res/layout/edit_text.xml30
-rw-r--r--packages/EasterEgg/res/layout/food_layout.xml31
-rw-r--r--packages/EasterEgg/res/layout/neko_activity.xml25
-rw-r--r--packages/EasterEgg/res/values/cat_strings.xml71
-rw-r--r--packages/EasterEgg/res/values/dimens.xml19
-rw-r--r--packages/EasterEgg/res/values/strings.xml4
-rw-r--r--packages/EasterEgg/res/xml/filepaths.xml19
-rw-r--r--packages/EasterEgg/src/com/android/egg/neko/Cat.java524
-rw-r--r--packages/EasterEgg/src/com/android/egg/neko/Food.java61
-rw-r--r--packages/EasterEgg/src/com/android/egg/neko/NekoActivationActivity.java66
-rw-r--r--packages/EasterEgg/src/com/android/egg/neko/NekoControlsService.kt323
-rw-r--r--packages/EasterEgg/src/com/android/egg/neko/NekoDialog.java109
-rw-r--r--packages/EasterEgg/src/com/android/egg/neko/NekoLand.java340
-rw-r--r--packages/EasterEgg/src/com/android/egg/neko/NekoLockedActivity.java48
-rw-r--r--packages/EasterEgg/src/com/android/egg/neko/NekoService.java193
-rw-r--r--packages/EasterEgg/src/com/android/egg/neko/NekoTile.java116
-rw-r--r--packages/EasterEgg/src/com/android/egg/neko/PrefState.java104
-rw-r--r--packages/SystemUI/AndroidManifest.xml1
-rw-r--r--packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java9
-rw-r--r--packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java38
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java22
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java32
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java67
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenrecord/ScreenInternalAudioRecorder.java15
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java25
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java6
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java13
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java35
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java7
-rw-r--r--services/core/java/com/android/server/am/ActivityManagerService.java52
-rw-r--r--services/core/java/com/android/server/am/OomAdjuster.java29
-rw-r--r--services/core/java/com/android/server/am/ProcessRecord.java10
-rwxr-xr-xservices/core/java/com/android/server/audio/AudioService.java31
-rw-r--r--services/core/java/com/android/server/media/MediaSessionService.java11
-rw-r--r--services/core/java/com/android/server/pm/PackageInstallerSession.java64
-rw-r--r--services/core/java/com/android/server/pm/PackageManagerShellCommand.java2
-rw-r--r--services/core/java/com/android/server/pm/permission/PermissionManagerService.java30
-rw-r--r--services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java2
-rw-r--r--services/core/java/com/android/server/wm/AccessibilityController.java3
-rw-r--r--services/core/java/com/android/server/wm/DisplayContent.java5
-rw-r--r--services/core/java/com/android/server/wm/InsetsSourceProvider.java6
-rw-r--r--services/core/java/com/android/server/wm/TaskDisplayArea.java17
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java17
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java26
-rwxr-xr-xservices/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java3
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java37
130 files changed, 4345 insertions, 351 deletions
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 5c3e4170391d..0b760a621d22 100644
--- a/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java
@@ -418,6 +418,26 @@ class BlobMetadata {
return revocableFd.getRevocableFileDescriptor();
}
+ void destroy() {
+ revokeAllFds();
+ getBlobFile().delete();
+ }
+
+ private void revokeAllFds() {
+ synchronized (mRevocableFds) {
+ for (int i = 0, pkgCount = mRevocableFds.size(); i < pkgCount; ++i) {
+ final ArraySet<RevocableFileDescriptor> packageFds =
+ mRevocableFds.valueAt(i);
+ if (packageFds == null) {
+ continue;
+ }
+ for (int j = 0, fdCount = packageFds.size(); j < fdCount; ++j) {
+ packageFds.valueAt(j).revoke();
+ }
+ }
+ }
+ }
+
boolean shouldBeDeleted(boolean respectLeaseWaitTime) {
// Expired data blobs
if (getBlobHandle().isExpired()) {
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 d75f46dde281..d9c0e478f1e3 100644
--- a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java
@@ -609,7 +609,11 @@ public class BlobStoreManagerService extends SystemService {
UserHandle.getUserId(callingUid));
userBlobs.entrySet().removeIf(entry -> {
final BlobMetadata blobMetadata = entry.getValue();
- return blobMetadata.getBlobId() == blobId;
+ if (blobMetadata.getBlobId() == blobId) {
+ deleteBlobLocked(blobMetadata);
+ return true;
+ }
+ return false;
});
writeBlobsInfoAsync();
}
@@ -660,11 +664,10 @@ public class BlobStoreManagerService extends SystemService {
switch (session.getState()) {
case STATE_ABANDONED:
case STATE_VERIFIED_INVALID:
- session.getSessionFile().delete();
synchronized (mBlobsLock) {
+ deleteSessionLocked(session);
getUserSessionsLocked(UserHandle.getUserId(session.getOwnerUid()))
.remove(session.getSessionId());
- mActiveBlobIds.remove(session.getSessionId());
if (LOGV) {
Slog.v(TAG, "Session is invalid; deleted " + session);
}
@@ -685,8 +688,7 @@ public class BlobStoreManagerService extends SystemService {
Slog.d(TAG, "Failed to commit: too many committed blobs. count: "
+ committedBlobsCount + "; blob: " + session);
session.sendCommitCallbackResult(COMMIT_RESULT_ERROR);
- session.getSessionFile().delete();
- mActiveBlobIds.remove(session.getSessionId());
+ deleteSessionLocked(session);
getUserSessionsLocked(UserHandle.getUserId(session.getOwnerUid()))
.remove(session.getSessionId());
break;
@@ -735,8 +737,7 @@ public class BlobStoreManagerService extends SystemService {
}
// Delete redundant data from recommits.
if (session.getSessionId() != blob.getBlobId()) {
- session.getSessionFile().delete();
- mActiveBlobIds.remove(session.getSessionId());
+ deleteSessionLocked(session);
}
getUserSessionsLocked(UserHandle.getUserId(session.getOwnerUid()))
.remove(session.getSessionId());
@@ -1022,8 +1023,7 @@ public class BlobStoreManagerService extends SystemService {
userSessions.removeIf((sessionId, blobStoreSession) -> {
if (blobStoreSession.getOwnerUid() == uid
&& blobStoreSession.getOwnerPackageName().equals(packageName)) {
- blobStoreSession.getSessionFile().delete();
- mActiveBlobIds.remove(blobStoreSession.getSessionId());
+ deleteSessionLocked(blobStoreSession);
return true;
}
return false;
@@ -1064,8 +1064,7 @@ public class BlobStoreManagerService extends SystemService {
if (userSessions != null) {
for (int i = 0, count = userSessions.size(); i < count; ++i) {
final BlobStoreSession session = userSessions.valueAt(i);
- session.getSessionFile().delete();
- mActiveBlobIds.remove(session.getSessionId());
+ deleteSessionLocked(session);
}
}
@@ -1144,8 +1143,7 @@ public class BlobStoreManagerService extends SystemService {
}
if (shouldRemove) {
- blobStoreSession.getSessionFile().delete();
- mActiveBlobIds.remove(blobStoreSession.getSessionId());
+ deleteSessionLocked(blobStoreSession);
deletedBlobIds.add(blobStoreSession.getSessionId());
}
return shouldRemove;
@@ -1157,8 +1155,14 @@ public class BlobStoreManagerService extends SystemService {
}
@GuardedBy("mBlobsLock")
+ private void deleteSessionLocked(BlobStoreSession blobStoreSession) {
+ blobStoreSession.destroy();
+ mActiveBlobIds.remove(blobStoreSession.getSessionId());
+ }
+
+ @GuardedBy("mBlobsLock")
private void deleteBlobLocked(BlobMetadata blobMetadata) {
- blobMetadata.getBlobFile().delete();
+ blobMetadata.destroy();
mActiveBlobIds.remove(blobMetadata.getBlobId());
}
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 53e296ba3d11..2f83be1e0370 100644
--- a/apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java
@@ -479,6 +479,11 @@ class BlobStoreSession extends IBlobStoreSession.Stub {
}
}
+ void destroy() {
+ revokeAllFds();
+ getSessionFile().delete();
+ }
+
private void revokeAllFds() {
synchronized (mRevocableFds) {
for (int i = mRevocableFds.size() - 1; i >= 0; --i) {
diff --git a/apex/media/framework/java/android/media/MediaParser.java b/apex/media/framework/java/android/media/MediaParser.java
index 1cb212167497..0c8c9a9c63e0 100644
--- a/apex/media/framework/java/android/media/MediaParser.java
+++ b/apex/media/framework/java/android/media/MediaParser.java
@@ -870,6 +870,14 @@ public final class MediaParser {
*/
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.
@@ -1310,6 +1318,10 @@ public final class MediaParser {
return new MatroskaExtractor(flags);
case PARSER_NAME_FMP4:
flags |=
+ getBooleanParameter(PARAMETER_EXPOSE_EMSG_TRACK)
+ ? FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK
+ : 0;
+ flags |=
getBooleanParameter(PARAMETER_MP4_IGNORE_EDIT_LISTS)
? FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_EDIT_LISTS
: 0;
@@ -1920,8 +1932,10 @@ public final class MediaParser {
// format for convenient use from ExoPlayer.
result.setString("crypto-mode-fourcc", format.drmInitData.schemeType);
}
+ if (format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) {
+ result.setLong("subsample-offset-us-long", format.subsampleOffsetUs);
+ }
// LACK OF SUPPORT FOR:
- // format.containerMimeType;
// format.id;
// format.metadata;
// format.stereoMode;
@@ -2106,6 +2120,7 @@ public final class MediaParser {
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.
diff --git a/apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/libstats/LibStatsPullTests.java b/apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/libstats/LibStatsPullTests.java
index 240222ea9411..6108a324e15e 100644
--- a/apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/libstats/LibStatsPullTests.java
+++ b/apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/libstats/LibStatsPullTests.java
@@ -33,7 +33,6 @@ import com.android.internal.os.StatsdConfigProto.PullAtomPackages;
import com.android.internal.os.StatsdConfigProto.SimpleAtomMatcher;
import com.android.internal.os.StatsdConfigProto.StatsdConfig;
import com.android.internal.os.StatsdConfigProto.TimeUnit;
-import com.android.internal.os.statsd.StatsConfigUtils;
import com.android.internal.os.statsd.protos.TestAtoms;
import com.android.os.AtomsProto.Atom;
diff --git a/apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/StatsConfigUtils.java b/apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/libstats/StatsConfigUtils.java
index d0d140092586..b5afb94886de 100644
--- a/apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/StatsConfigUtils.java
+++ b/apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/libstats/StatsConfigUtils.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.internal.os.statsd;
+package com.android.internal.os.statsd.libstats;
import static com.google.common.truth.Truth.assertThat;
diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto
index ca03343ddb80..02c0763c9d83 100644
--- a/cmds/statsd/src/atoms.proto
+++ b/cmds/statsd/src/atoms.proto
@@ -9694,6 +9694,7 @@ message RuntimeAppOpAccess {
UNIFORM = 1;
RARELY_USED = 2;
BOOT_TIME_SAMPLING = 3;
+ UNIFORM_OPS = 4;
}
// sampling strategy used to collect this message
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index 6f8233d5de9b..c9031b711657 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -763,26 +763,27 @@ public class ApplicationPackageManager extends PackageManager {
@Override
public void revokeRuntimePermission(String packageName, String permName, UserHandle user) {
+ revokeRuntimePermission(packageName, permName, user, null);
+ }
+
+ @Override
+ public void revokeRuntimePermission(String packageName, String permName, UserHandle user,
+ String reason) {
if (DEBUG_TRACE_PERMISSION_UPDATES
&& shouldTraceGrant(packageName, permName, user.getIdentifier())) {
Log.i(TAG, "App " + mContext.getPackageName() + " is revoking " + packageName + " "
- + permName + " for user " + user.getIdentifier(), new RuntimeException());
+ + permName + " for user " + user.getIdentifier() + " with reason " + reason,
+ new RuntimeException());
}
try {
mPermissionManager
- .revokeRuntimePermission(packageName, permName, user.getIdentifier());
+ .revokeRuntimePermission(packageName, permName, user.getIdentifier(), reason);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
@Override
- public void revokeRuntimePermission(String packageName, String permName, UserHandle user,
- String reason) {
- // TODO evanseverson: impl
- }
-
- @Override
public int getPermissionFlags(String permName, String packageName, UserHandle user) {
try {
return mPermissionManager
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index a828aac78ded..86a3579effe1 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -1900,13 +1900,11 @@ class ContextImpl extends Context {
@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 @@ class ContextImpl extends Context {
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 e84c5e574713..945957738f8e 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -677,4 +677,10 @@ interface IActivityManager {
* Return whether the app freezer is supported (true) or not (false) by this system.
*/
boolean isAppFreezerSupported();
+
+
+ /**
+ * Kills uid with the reason of permission change.
+ */
+ void killUidForPermissionChange(int appId, int userId, String reason);
}
diff --git a/core/java/android/app/UiAutomationConnection.java b/core/java/android/app/UiAutomationConnection.java
index 82e988109db8..ce51dba76780 100644
--- a/core/java/android/app/UiAutomationConnection.java
+++ b/core/java/android/app/UiAutomationConnection.java
@@ -294,7 +294,7 @@ public final class UiAutomationConnection extends IUiAutomationConnection.Stub {
}
final long identity = Binder.clearCallingIdentity();
try {
- mPermissionManager.revokeRuntimePermission(packageName, permission, userId);
+ mPermissionManager.revokeRuntimePermission(packageName, permission, userId, null);
} finally {
Binder.restoreCallingIdentity(identity);
}
diff --git a/core/java/android/permission/IPermissionManager.aidl b/core/java/android/permission/IPermissionManager.aidl
index 235b0830b9aa..e23102113e9f 100644
--- a/core/java/android/permission/IPermissionManager.aidl
+++ b/core/java/android/permission/IPermissionManager.aidl
@@ -71,7 +71,7 @@ interface IPermissionManager {
void grantRuntimePermission(String packageName, String permName, int userId);
- void revokeRuntimePermission(String packageName, String permName, int userId);
+ void revokeRuntimePermission(String packageName, String permName, int userId, String reason);
void resetRuntimePermissions();
diff --git a/core/java/android/view/InsetsState.java b/core/java/android/view/InsetsState.java
index 9bf2e01a6bd1..91e7591193f1 100644
--- a/core/java/android/view/InsetsState.java
+++ b/core/java/android/view/InsetsState.java
@@ -427,8 +427,7 @@ public class InsetsState implements Parcelable {
if (copySources) {
for (int i = 0; i < SIZE; i++) {
InsetsSource source = other.mSources[i];
- if (source == null) continue;
- mSources[i] = new InsetsSource(source);
+ mSources[i] = source != null ? new InsetsSource(source) : null;
}
} else {
for (int i = 0; i < SIZE; i++) {
diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java
index ffeeb806ba54..0d2d4d13eb38 100644
--- a/core/java/android/view/ViewConfiguration.java
+++ b/core/java/android/view/ViewConfiguration.java
@@ -500,13 +500,12 @@ public class ViewConfiguration {
*/
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 1b666aa67e90..9712311aab7c 100644
--- a/core/java/android/widget/inline/InlineContentView.java
+++ b/core/java/android/widget/inline/InlineContentView.java
@@ -21,8 +21,8 @@ import android.annotation.Nullable;
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;
@@ -156,7 +156,8 @@ public class InlineContentView extends ViewGroup {
@Override
public void onDraw() {
computeParentPositionAndScale();
- mSurfaceView.setVisibility(VISIBLE);
+ final int visibility = InlineContentView.this.isShown() ? VISIBLE : GONE;
+ mSurfaceView.setVisibility(visibility);
}
};
diff --git a/core/java/com/android/internal/app/ChooserListAdapter.java b/core/java/com/android/internal/app/ChooserListAdapter.java
index 5efd46c4c64a..de5ab6f1c90d 100644
--- a/core/java/com/android/internal/app/ChooserListAdapter.java
+++ b/core/java/com/android/internal/app/ChooserListAdapter.java
@@ -561,7 +561,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
mChooserTargetScores.put(componentName, new HashMap<>());
}
mChooserTargetScores.get(componentName).put(shortcutInfo.getShortLabel().toString(),
- shortcutInfo.getRank());
+ target.getRank());
}
mChooserTargetScores.keySet().forEach(key -> rankTargetsWithinComponent(key));
}
diff --git a/core/java/com/android/internal/app/PlatLogoActivity.java b/core/java/com/android/internal/app/PlatLogoActivity.java
index 2a7eae626795..986bbc8628ec 100644
--- a/core/java/com/android/internal/app/PlatLogoActivity.java
+++ b/core/java/com/android/internal/app/PlatLogoActivity.java
@@ -55,6 +55,10 @@ import org.json.JSONObject;
public class PlatLogoActivity extends Activity {
private static final boolean WRITE_SETTINGS = true;
+ private static final String R_EGG_UNLOCK_SETTING = "egg_mode_r";
+
+ private static final int UNLOCK_TRIES = 3;
+
BigDialView mDialView;
@Override
@@ -77,8 +81,10 @@ public class PlatLogoActivity extends Activity {
mDialView = new BigDialView(this, null);
if (Settings.System.getLong(getContentResolver(),
- "egg_mode" /* Settings.System.EGG_MODE */, 0) == 0) {
- mDialView.setUnlockTries(3);
+ R_EGG_UNLOCK_SETTING, 0) == 0) {
+ mDialView.setUnlockTries(UNLOCK_TRIES);
+ } else {
+ mDialView.setUnlockTries(0);
}
final FrameLayout layout = new FrameLayout(this);
@@ -91,18 +97,16 @@ public class PlatLogoActivity extends Activity {
private void launchNextStage(boolean locked) {
final ContentResolver cr = getContentResolver();
- if (Settings.System.getLong(cr, "egg_mode" /* Settings.System.EGG_MODE */, 0) == 0) {
- // For posterity: the moment this user unlocked the easter egg
- try {
- if (WRITE_SETTINGS) {
- Settings.System.putLong(cr,
- "egg_mode", // Settings.System.EGG_MODE,
- locked ? 0 : System.currentTimeMillis());
- }
- } catch (RuntimeException e) {
- Log.e("com.android.internal.app.PlatLogoActivity", "Can't write settings", e);
+ try {
+ if (WRITE_SETTINGS) {
+ Settings.System.putLong(cr,
+ R_EGG_UNLOCK_SETTING,
+ locked ? 0 : System.currentTimeMillis());
}
+ } catch (RuntimeException e) {
+ Log.e("com.android.internal.app.PlatLogoActivity", "Can't write settings", e);
}
+
try {
startActivity(new Intent(Intent.ACTION_MAIN)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
@@ -235,8 +239,8 @@ public class PlatLogoActivity extends Activity {
}
return true;
case MotionEvent.ACTION_UP:
- if (mWasLocked && !mDialDrawable.isLocked()) {
- launchNextStage(false);
+ if (mWasLocked != mDialDrawable.isLocked()) {
+ launchNextStage(mDialDrawable.isLocked());
}
return true;
}
@@ -404,6 +408,8 @@ public class PlatLogoActivity extends Activity {
if (isLocked() && oldUserLevel != STEPS - 1 && getUserLevel() == STEPS - 1) {
mUnlockTries--;
+ } else if (!isLocked() && getUserLevel() == 0) {
+ mUnlockTries = UNLOCK_TRIES;
}
if (!isLocked()) {
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index 86c13a0581c2..fba4675a8c9f 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -20,9 +20,6 @@ import static android.Manifest.permission.INTERACT_ACROSS_PROFILES;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.content.PermissionChecker.PID_UNKNOWN;
-import static com.android.internal.app.AbstractMultiProfilePagerAdapter.PROFILE_PERSONAL;
-import static com.android.internal.app.AbstractMultiProfilePagerAdapter.PROFILE_WORK;
-
import android.annotation.Nullable;
import android.annotation.StringRes;
import android.annotation.UiThread;
@@ -159,6 +156,9 @@ public class ResolverActivity extends Activity implements
protected static final String METRICS_CATEGORY_RESOLVER = "intent_resolver";
protected static final String METRICS_CATEGORY_CHOOSER = "intent_chooser";
+ /** Tracks if we should ignore future broadcasts telling us the work profile is enabled */
+ private boolean mWorkProfileHasBeenEnabled = false;
+
@VisibleForTesting
public static boolean ENABLE_TABBED_VIEW = true;
private static final String TAB_TAG_PERSONAL = "personal";
@@ -825,12 +825,23 @@ public class ResolverActivity extends Activity implements
if (shouldShowTabs()) {
mWorkProfileStateReceiver = createWorkProfileStateReceiver();
registerWorkProfileStateReceiver();
+
+ mWorkProfileHasBeenEnabled = isWorkProfileEnabled();
}
}
+ private boolean isWorkProfileEnabled() {
+ UserHandle workUserHandle = getWorkProfileUserHandle();
+ UserManager userManager = getSystemService(UserManager.class);
+
+ return !userManager.isQuietModeEnabled(workUserHandle)
+ && userManager.isUserUnlocked(workUserHandle);
+ }
+
private void registerWorkProfileStateReceiver() {
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_USER_UNLOCKED);
+ filter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE);
filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE);
registerReceiverAsUser(mWorkProfileStateReceiver, UserHandle.ALL, filter, null, null);
}
@@ -1961,17 +1972,29 @@ public class ResolverActivity extends Activity implements
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (!TextUtils.equals(action, Intent.ACTION_USER_UNLOCKED)
- && !TextUtils.equals(action, Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)) {
+ && !TextUtils.equals(action, Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)
+ && !TextUtils.equals(action, Intent.ACTION_MANAGED_PROFILE_AVAILABLE)) {
return;
}
- int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
- if (TextUtils.equals(action, Intent.ACTION_USER_UNLOCKED)
- && userHandle != getWorkProfileUserHandle().getIdentifier()) {
+
+ int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+
+ if (userId != getWorkProfileUserHandle().getIdentifier()) {
return;
}
- if (TextUtils.equals(action, Intent.ACTION_USER_UNLOCKED)) {
+
+ if (isWorkProfileEnabled()) {
+ if (mWorkProfileHasBeenEnabled) {
+ return;
+ }
+
+ mWorkProfileHasBeenEnabled = true;
mMultiProfilePagerAdapter.markWorkProfileEnabledBroadcastReceived();
+ } else {
+ // Must be an UNAVAILABLE broadcast, so we watch for the next availability
+ mWorkProfileHasBeenEnabled = false;
}
+
if (mMultiProfilePagerAdapter.getCurrentUserHandle()
.equals(getWorkProfileUserHandle())) {
mMultiProfilePagerAdapter.rebuildActiveTab(true);
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 9c1ecf2e48e0..9945057f0e94 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -4850,7 +4850,7 @@
<!-- @SystemApi Allows an application to turn on / off quiet mode.
@hide -->
<permission android:name="android.permission.MODIFY_QUIET_MODE"
- android:protectionLevel="signature|privileged|wellbeing" />
+ android:protectionLevel="signature|privileged|wellbeing|development" />
<!-- Allows internal management of the camera framework
@hide -->
diff --git a/core/res/res/layout/notification_template_material_conversation.xml b/core/res/res/layout/notification_template_material_conversation.xml
index 82e99e6419b6..861a056f3569 100644
--- a/core/res/res/layout/notification_template_material_conversation.xml
+++ b/core/res/res/layout/notification_template_material_conversation.xml
@@ -128,6 +128,9 @@
android:layout_weight="1">
<!-- Header -->
+
+ <!-- Use layout_marginStart instead of paddingStart to work around strange
+ measurement behavior on lower display densities. -->
<LinearLayout
android:id="@+id/conversation_header"
android:layout_width="wrap_content"
@@ -135,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/core/tests/coretests/src/android/view/InsetsStateTest.java b/core/tests/coretests/src/android/view/InsetsStateTest.java
index 7115acfedcf6..5260ef83cc4f 100644
--- a/core/tests/coretests/src/android/view/InsetsStateTest.java
+++ b/core/tests/coretests/src/android/view/InsetsStateTest.java
@@ -304,6 +304,7 @@ public class InsetsStateTest {
mState.getSource(ITYPE_IME).setVisibleFrame(new Rect(0, 0, 50, 10));
mState.getSource(ITYPE_IME).setVisible(true);
mState.getSource(ITYPE_STATUS_BAR).setFrame(new Rect(0, 0, 100, 100));
+ mState2.getSource(ITYPE_NAVIGATION_BAR).setFrame(new Rect(0, 0, 100, 100));
mState2.set(mState, true);
assertEquals(mState, mState2);
}
diff --git a/identity/java/android/security/identity/IdentityCredential.java b/identity/java/android/security/identity/IdentityCredential.java
index 493c85a930be..4eb6e420c07f 100644
--- a/identity/java/android/security/identity/IdentityCredential.java
+++ b/identity/java/android/security/identity/IdentityCredential.java
@@ -41,19 +41,18 @@ public abstract class IdentityCredential {
/**
* Create an ephemeral key pair to use to establish a secure channel with a reader.
*
- * <p>Most applications will use only the public key, and only to send it to the reader,
- * allowing the private key to be used internally for {@link #encryptMessageToReader(byte[])}
- * and {@link #decryptMessageFromReader(byte[])}. The private key is also provided for
- * applications that wish to use a cipher suite that is not supported by
- * {@link IdentityCredentialStore}.
+ * <p>Applications should use this key-pair for the communications channel with the reader
+ * using a protocol / cipher-suite appropriate for the application. One example of such a
+ * protocol is the one used for Mobile Driving Licenses, see ISO 18013-5 section 9.2.1 "Session
+ * encryption".
*
* @return ephemeral key pair to use to establish a secure channel with a reader.
*/
public @NonNull abstract KeyPair createEphemeralKeyPair();
/**
- * Set the ephemeral public key provided by the reader. This must be called before
- * {@link #encryptMessageToReader} or {@link #decryptMessageFromReader} can be called.
+ * Set the ephemeral public key provided by the reader. If called, this must be called before
+ * {@link #getEntries(byte[], Map, byte[], byte[])} is called.
*
* @param readerEphemeralPublicKey The ephemeral public key provided by the reader to
* establish a secure session.
@@ -65,6 +64,11 @@ public abstract class IdentityCredential {
/**
* Encrypt a message for transmission to the reader.
*
+ * <p>Do not use. In this version of the API, this method produces an incorrect
+ * result. Instead, applications should implement message encryption/decryption themselves as
+ * detailed in the {@link #createEphemeralKeyPair()} method. In a future API-level, this
+ * method will be deprecated.
+ *
* @param messagePlaintext unencrypted message to encrypt.
* @return encrypted message.
*/
@@ -73,6 +77,11 @@ public abstract class IdentityCredential {
/**
* Decrypt a message received from the reader.
*
+ * <p>Do not use. In this version of the API, this method produces an incorrect
+ * result. Instead, applications should implement message encryption/decryption themselves as
+ * detailed in the {@link #createEphemeralKeyPair()} method. In a future API-level, this
+ * method will be deprecated.
+ *
* @param messageCiphertext encrypted message to decrypt.
* @return decrypted message.
* @throws MessageDecryptionException if the ciphertext couldn't be decrypted.
@@ -178,7 +187,7 @@ public abstract class IdentityCredential {
*
* <p>If {@code readerAuth} is not {@code null} it must be the bytes of a {@code COSE_Sign1}
* structure as defined in RFC 8152. For the payload nil shall be used and the
- * detached payload is the ReaderAuthentication CBOR described below.
+ * detached payload is the ReaderAuthenticationBytes CBOR described below.
* <pre>
* ReaderAuthentication = [
* "ReaderAuthentication",
@@ -186,7 +195,9 @@ public abstract class IdentityCredential {
* ItemsRequestBytes
* ]
*
- * ItemsRequestBytes = #6.24(bstr .cbor ItemsRequest) ; Bytes of ItemsRequest
+ * ItemsRequestBytes = #6.24(bstr .cbor ItemsRequest)
+ *
+ * ReaderAuthenticationBytes = #6.24(bstr .cbor ReaderAuthentication)
* </pre>
*
* <p>where {@code ItemsRequestBytes} are the bytes in the {@code requestMessage} parameter.
diff --git a/identity/java/android/security/identity/ResultData.java b/identity/java/android/security/identity/ResultData.java
index 37de2c4a50ea..71860d261285 100644
--- a/identity/java/android/security/identity/ResultData.java
+++ b/identity/java/android/security/identity/ResultData.java
@@ -68,8 +68,8 @@ public abstract class ResultData {
* {@link #getMessageAuthenticationCode()} can be used to get a MAC.
*
* <p>The CBOR structure which is cryptographically authenticated is the
- * {@code DeviceAuthentication} structure according to the following
- * <a href="https://tools.ietf.org/html/draft-ietf-cbor-cddl-06">CDDL</a> schema:
+ * {@code DeviceAuthenticationBytes} structure according to the following
+ * <a href="https://tools.ietf.org/html/rfc8610">CDDL</a> schema:
*
* <pre>
* DeviceAuthentication = [
@@ -80,15 +80,9 @@ public abstract class ResultData {
* ]
*
* DocType = tstr
- *
- * SessionTranscript = [
- * DeviceEngagementBytes,
- * EReaderKeyBytes
- * ]
- *
- * DeviceEngagementBytes = #6.24(bstr .cbor DeviceEngagement)
- * EReaderKeyBytes = #6.24(bstr .cbor EReaderKey.Pub)
+ * SessionTranscript = any
* DeviceNameSpacesBytes = #6.24(bstr .cbor DeviceNameSpaces)
+ * DeviceAuthenticationBytes = #6.24(bstr .cbor DeviceAuthentication)
* </pre>
*
* <p>where
@@ -115,7 +109,7 @@ public abstract class ResultData {
public abstract @NonNull byte[] getAuthenticatedData();
/**
- * Returns a message authentication code over the {@code DeviceAuthentication} CBOR
+ * Returns a message authentication code over the {@code DeviceAuthenticationBytes} CBOR
* specified in {@link #getAuthenticatedData()}, to prove to the reader that the data
* is from a trusted credential.
*
diff --git a/packages/CarSystemUI/res/layout/notification_center_activity.xml b/packages/CarSystemUI/res/layout/notification_center_activity.xml
index 0af74c4462a6..0e45e43132de 100644
--- a/packages/CarSystemUI/res/layout/notification_center_activity.xml
+++ b/packages/CarSystemUI/res/layout/notification_center_activity.xml
@@ -22,6 +22,10 @@
android:layout_height="match_parent"
android:background="@color/notification_shade_background_color">
+ <com.android.car.ui.FocusParkingView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
<View
android:id="@+id/glass_pane"
android:layout_width="match_parent"
@@ -33,16 +37,20 @@
app:layout_constraintTop_toTopOf="parent"
/>
- <androidx.recyclerview.widget.RecyclerView
- android:id="@+id/notifications"
+ <com.android.car.ui.FocusArea
android:layout_width="0dp"
android:layout_height="0dp"
android:orientation="vertical"
- android:paddingBottom="@dimen/notification_shade_list_padding_bottom"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent"/>
+ app:layout_constraintTop_toTopOf="parent">
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/notifications"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingBottom="@dimen/notification_shade_list_padding_bottom"/>
+ </com.android.car.ui.FocusArea>
<include layout="@layout/notification_handle_bar"/>
diff --git a/packages/EasterEgg/Android.bp b/packages/EasterEgg/Android.bp
index 43ed810b5674..b858ab01ffd9 100644
--- a/packages/EasterEgg/Android.bp
+++ b/packages/EasterEgg/Android.bp
@@ -23,11 +23,23 @@ android_app {
name: "EasterEgg",
+ platform_apis: true,
certificate: "platform",
- sdk_version: "current",
-
optimize: {
enabled: false,
- }
+ },
+
+ static_libs: [
+ "androidx.core_core",
+ "androidx.recyclerview_recyclerview",
+ "androidx.annotation_annotation",
+ "kotlinx-coroutines-android",
+ "kotlinx-coroutines-core",
+ //"kotlinx-coroutines-reactive",
+ ],
+
+ manifest: "AndroidManifest.xml",
+
+ kotlincflags: ["-Xjvm-default=enable"],
}
diff --git a/packages/EasterEgg/AndroidManifest.xml b/packages/EasterEgg/AndroidManifest.xml
index 7f76a4529963..57c459b6f0fd 100644
--- a/packages/EasterEgg/AndroidManifest.xml
+++ b/packages/EasterEgg/AndroidManifest.xml
@@ -6,19 +6,24 @@
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
+ <!-- used for cat notifications -->
+ <uses-permission android:name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME" />
+ <!-- used to save cat images -->
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <!-- controls -->
+ <uses-permission android:name="android.permission.BIND_CONTROLS" />
+
<application
- android:icon="@drawable/q_icon"
+ android:icon="@drawable/icon"
android:label="@string/app_name">
+
<activity android:name=".quares.QuaresActivity"
android:icon="@drawable/q_icon"
android:label="@string/q_egg_name"
+ android:exported="true"
android:theme="@style/QuaresTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
-
- <category android:name="android.intent.category.DEFAULT" />
- <!-- <category android:name="android.intent.category.LAUNCHER" /> -->
- <category android:name="com.android.internal.category.PLATLOGO" />
</intent-filter>
</activity>
<activity
@@ -26,15 +31,86 @@
android:configChanges="orientation|keyboardHidden|screenSize|uiMode"
android:icon="@drawable/p_icon"
android:label="@string/p_egg_name"
+ android:exported="true"
android:theme="@style/AppTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
+ </intent-filter>
+ </activity>
- <!-- <category android:name="android.intent.category.DEFAULT" /> -->
- <!-- <category android:name="android.intent.category.LAUNCHER" /> -->
- <!-- <category android:name="com.android.internal.category.PLATLOGO" /> -->
+ <!-- Android N easter egg bits -->
+ <activity android:name=".neko.NekoLand"
+ android:theme="@android:style/Theme.Material.NoActionBar"
+ android:exported="true"
+ android:label="@string/app_name">
+ <intent-filter>
+ <action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
+
+ <!-- This is where the magic happens -->
+ <service
+ android:name=".neko.NekoService"
+ android:enabled="true"
+ android:permission="android.permission.BIND_JOB_SERVICE"
+ android:exported="true" >
+ </service>
+
+ <!-- Used to show over lock screen -->
+ <activity android:name=".neko.NekoLockedActivity"
+ android:excludeFromRecents="true"
+ android:exported="true"
+ android:theme="@android:style/Theme.Material.Light.Dialog.NoActionBar"
+ android:showOnLockScreen="true" />
+
+ <!-- Used to enable easter egg -->
+ <activity android:name=".neko.NekoActivationActivity"
+ android:excludeFromRecents="true"
+ android:exported="true"
+ android:theme="@android:style/Theme.NoDisplay"
+ >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="com.android.internal.category.PLATLOGO" />
+ </intent-filter>
+ </activity>
+
+ <!-- The quick settings tile, disabled by default -->
+ <service
+ android:name=".neko.NekoTile"
+ android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
+ android:icon="@drawable/stat_icon"
+ android:enabled="false"
+ android:label="@string/default_tile_name">
+ <intent-filter>
+ <action android:name="android.service.quicksettings.action.QS_TILE" />
+ </intent-filter>
+ </service>
+
+ <service android:name=".neko.NekoControlsService"
+ android:permission="android.permission.BIND_CONTROLS"
+ android:label="@string/r_egg_name"
+ android:icon="@drawable/ic_fullcat_icon"
+ android:enabled="false"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.service.controls.ControlsProviderService" />
+ </intent-filter>
+ </service>
+
+ <!-- FileProvider for sending pictures -->
+ <provider
+ android:name="androidx.core.content.FileProvider"
+ android:authorities="com.android.egg.fileprovider"
+ android:grantUriPermissions="true"
+ android:exported="false">
+ <meta-data
+ android:name="android.support.FILE_PROVIDER_PATHS"
+ android:resource="@xml/filepaths" />
+ </provider>
</application>
</manifest>
diff --git a/packages/EasterEgg/build.gradle b/packages/EasterEgg/build.gradle
new file mode 100644
index 000000000000..20b469898498
--- /dev/null
+++ b/packages/EasterEgg/build.gradle
@@ -0,0 +1,82 @@
+buildscript {
+ ext.kotlin_version = '1.3.71'
+
+ repositories {
+ google()
+ jcenter()
+ }
+
+ dependencies {
+ classpath 'com.android.tools.build:gradle:4.0.0'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+}
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
+
+final String ANDROID_ROOT = "${rootDir}/../../../.."
+
+android {
+ compileSdkVersion COMPILE_SDK
+ buildToolsVersion BUILD_TOOLS_VERSION
+
+ defaultConfig {
+ applicationId "com.android.egg"
+ minSdkVersion 28
+ targetSdkVersion 30
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ sourceSets {
+ main {
+ res.srcDirs = ['res']
+ java.srcDirs = ['src']
+ manifest.srcFile 'AndroidManifest.xml'
+ }
+ }
+
+ signingConfigs {
+ debug.storeFile file("${ANDROID_ROOT}/vendor/google/certs/devkeys/platform.keystore")
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+
+
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation 'androidx.appcompat:appcompat:1.1.0'
+ implementation 'androidx.core:core-ktx:1.2.0'
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6'
+ implementation "androidx.recyclerview:recyclerview:${ANDROID_X_VERSION}"
+ implementation "androidx.dynamicanimation:dynamicanimation:${ANDROID_X_VERSION}"
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+ androidTestImplementation "androidx.annotation:annotation:${ANDROID_X_VERSION}"
+}
+
diff --git a/packages/EasterEgg/gradle.properties b/packages/EasterEgg/gradle.properties
new file mode 100644
index 000000000000..e8e6450e2943
--- /dev/null
+++ b/packages/EasterEgg/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+android.enableJetifier=true
+kotlin.code.style=official
+
+ANDROID_X_VERSION=1+
+COMPILE_SDK=android-30
+BUILD_TOOLS_VERSION=28.0.3
diff --git a/packages/EasterEgg/res/drawable/android_11_dial.xml b/packages/EasterEgg/res/drawable/android_11_dial.xml
new file mode 100644
index 000000000000..73fd37f1bdd6
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/android_11_dial.xml
@@ -0,0 +1,63 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <path
+ android:pathData="M77.773,51.064h-1.583c-0.217,0 -0.393,-0.176 -0.393,-0.393v-1.46c0,-0.217 0.176,-0.393 0.393,-0.393h3.466c0.217,0 0.393,0.176 0.393,0.393v9.921c0,0.217 -0.176,0.393 -0.393,0.393h-1.49c-0.217,0 -0.393,-0.176 -0.393,-0.393V51.064z"
+ android:fillColor="#F86734"/>
+ <path
+ android:pathData="M83.598,51.064h-1.583c-0.217,0 -0.393,-0.176 -0.393,-0.393v-1.46c0,-0.217 0.176,-0.393 0.393,-0.393h3.466c0.217,0 0.393,0.176 0.393,0.393v9.921c0,0.217 -0.176,0.393 -0.393,0.393h-1.49c-0.217,0 -0.393,-0.176 -0.393,-0.393V51.064z"
+ android:fillColor="#F86734"/>
+ <path
+ android:pathData="M70.044,75.974m-0.644,0a0.644,0.644 0,1 1,1.288 0a0.644,0.644 0,1 1,-1.288 0"
+ android:fillColor="#d7effe"/>
+ <path
+ android:pathData="M56.896,80.985m-0.718,0a0.718,0.718 0,1 1,1.436 0a0.718,0.718 0,1 1,-1.436 0"
+ android:fillColor="#d7effe"/>
+ <path
+ android:pathData="M43.408,78.881m-0.795,0a0.795,0.795 0,1 1,1.59 0a0.795,0.795 0,1 1,-1.59 0"
+ android:fillColor="#d7effe"/>
+ <path
+ android:pathData="M32.419,70.115m-0.874,0a0.874,0.874 0,1 1,1.748 0a0.874,0.874 0,1 1,-1.748 0"
+ android:fillColor="#d7effe"/>
+ <path
+ android:pathData="M27.306,56.992m-0.954,0a0.954,0.954 0,1 1,1.908 0a0.954,0.954 0,1 1,-1.908 0"
+ android:fillColor="#d7effe"/>
+ <path
+ android:pathData="M29.313,43.489m-1.036,0a1.036,1.036 0,1 1,2.072 0a1.036,1.036 0,1 1,-2.072 0"
+ android:fillColor="#d7effe"/>
+ <path
+ android:pathData="M37.988,32.445m-1.118,0a1.118,1.118 0,1 1,2.236 0a1.118,1.118 0,1 1,-2.236 0"
+ android:fillColor="#d7effe"/>
+ <path
+ android:pathData="M51.137,27.064m-1.201,0a1.201,1.201 0,1 1,2.402 0a1.201,1.201 0,1 1,-2.402 0"
+ android:fillColor="#d7effe"/>
+ <path
+ android:pathData="M64.553,28.868m-1.284,0a1.284,1.284 0,1 1,2.568 0a1.284,1.284 0,1 1,-2.568 0"
+ android:fillColor="#d7effe"/>
+ <path
+ android:pathData="M75.522,37.652m-1.368,0a1.368,1.368 0,1 1,2.736 0a1.368,1.368 0,1 1,-2.736 0"
+ android:fillColor="#d7effe"/>
+ <path
+ android:pathData="M87.942,115.052l-47.557,-47.557l26.869,-26.87l47.557,47.558z">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:startY="56.087"
+ android:startX="55.8464"
+ android:endY="100.0297"
+ android:endX="99.7891"
+ android:type="linear">
+ <item android:offset="0" android:color="#3F000000"/>
+ <item android:offset="1" android:color="#00000000"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path
+ android:pathData="M53.928,54.17m-18.999,0a18.999,18.999 0,1 1,37.998 0a18.999,18.999 0,1 1,-37.998 0"
+ android:fillColor="#3ddc84"/>
+ <path
+ android:pathData="M66.353,54.17m-3.185,0a3.185,3.185 0,1 1,6.37 0a3.185,3.185 0,1 1,-6.37 0"
+ android:fillColor="#FFFFFF"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/back.xml b/packages/EasterEgg/res/drawable/back.xml
new file mode 100644
index 000000000000..b55d65cdf76d
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/back.xml
@@ -0,0 +1,22 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path android:name="back" android:fillColor="#FF000000" android:pathData="M37.1,22c-1.1,0 -1.9,0.8 -1.9,1.9v5.6c0,1.1 0.8,1.9 1.9,1.9H39v-1.9v-5.6V22H37.1z"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/belly.xml b/packages/EasterEgg/res/drawable/belly.xml
new file mode 100644
index 000000000000..8b0e9afac463
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/belly.xml
@@ -0,0 +1,22 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path android:name="belly" android:fillColor="#FF000000" android:pathData="M20.5,25c-3.6,0 -6.5,2.9 -6.5,6.5V38h13v-6.5C27,27.9 24.1,25 20.5,25z"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/body.xml b/packages/EasterEgg/res/drawable/body.xml
new file mode 100644
index 000000000000..86087209eff5
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/body.xml
@@ -0,0 +1,22 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path android:name="body" android:fillColor="#FF000000" android:pathData="M9,20h30v18h-30z"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/bowtie.xml b/packages/EasterEgg/res/drawable/bowtie.xml
new file mode 100644
index 000000000000..33fa9216712f
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/bowtie.xml
@@ -0,0 +1,22 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path android:name="bowtie" android:fillColor="#FF000000" android:pathData="M29,16.8l-10,5l0,-5l10,5z"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/cap.xml b/packages/EasterEgg/res/drawable/cap.xml
new file mode 100644
index 000000000000..d8b4cc58a261
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/cap.xml
@@ -0,0 +1,22 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path android:name="cap" android:fillColor="#FF000000" android:pathData="M27.2,3.8c-1,-0.2 -2.1,-0.3 -3.2,-0.3s-2.1,0.1 -3.2,0.3c0.2,1.3 1.5,2.2 3.2,2.2C25.6,6.1 26.9,5.1 27.2,3.8z"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/collar.xml b/packages/EasterEgg/res/drawable/collar.xml
new file mode 100644
index 000000000000..5e4d0fd4f886
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/collar.xml
@@ -0,0 +1,22 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path android:name="collar" android:fillColor="#FF000000" android:pathData="M9,18.4h30v1.7h-30z"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/face_spot.xml b/packages/EasterEgg/res/drawable/face_spot.xml
new file mode 100644
index 000000000000..a89fb4fdaadd
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/face_spot.xml
@@ -0,0 +1,22 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path android:name="face_spot" android:fillColor="#FF000000" android:pathData="M19.5,15.2a4.5,3.2 0,1 0,9 0a4.5,3.2 0,1 0,-9 0z"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/food_bits.xml b/packages/EasterEgg/res/drawable/food_bits.xml
new file mode 100644
index 000000000000..1b2bb6f36947
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/food_bits.xml
@@ -0,0 +1,33 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M19.1,34l-3.5,1.3c-1,0.4,-2.2,-0.1,-2.6,-1.1l-1.2,-3c-0.4,-1,0.1,-2.2,1.1,-2.6l3.5,-1.3c1,-0.4,2.2,0.1,2.6,1.1l1.2,3 C20.6,32.4,20.1,33.6,19.1,34z"/>
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M25.2,28.1L22.9,28c-0.8,0,-1.5,-0.7,-1.4,-1.6l0.1,-2c0,-0.8,0.7,-1.5,1.6,-1.4l2.4,0.1c0.8,0,1.5,0.7,1.4,1.6l-0.1,2 C26.8,27.5,26.1,28.1,25.2,28.1z"/>
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M18.7,23.1L16.5,23c-0.5,0,-0.9,-0.4,-0.8,-0.9l0.1,-2.2c0,-0.5,0.4,-0.9,0.9,-0.8l2.2,0.1c0.5,0,0.9,0.4,0.8,0.9 l-0.1,2.2C19.6,22.8,19.2,23.1,18.7,23.1z"/>
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M32.2,35.3l-3.6,-1.8c-1,-0.5,-1.4,-1.7,-0.9,-2.7l1.6,-3.1c0.5,-1,1.7,-1.4,2.7,-0.9l3.6,1.8c1,0.5,1.4,1.7,0.9,2.7 l-1.6,3.1C34.4,35.4,33.2,35.7,32.2,35.3z"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/food_chicken.xml b/packages/EasterEgg/res/drawable/food_chicken.xml
new file mode 100644
index 000000000000..95b2fb55b796
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/food_chicken.xml
@@ -0,0 +1,39 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M9,12v14h10V11H9z M11.7,16.3c-0.7,0,-1.3,-0.6,-1.3,-1.3s0.6,-1.3,1.3,-1.3S13,14.3,13,15S12.4,16.3,11.7,16.3z"/>
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M5.7,20.1l1.6,-3.0l-1.6,-3.0l4.4,3.0z"/>
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M19.0,6.0l-2.3,2.3l-2.7,-2.6l-2.7,2.6l-2.3,-2.3l0.0,4.0l10.0,0.0z"/>
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M9,25c0,8.3,6.7,15,15,15s15,-6.7,15,-15H9z M29.9,31.5h-11v-1h12L29.9,31.5z M31.9,29.5h-13v-1h14L31.9,29.5z M33.9,27.5 h-15v-1h16L33.9,27.5z"/>
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M27.0,38.6h2.0v6.0h-2.0z"/>
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M17.4,44.6l-2.1999998,0.0l4.4000006,-6.0l2.1999989,0.0z"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/food_cookie.xml b/packages/EasterEgg/res/drawable/food_cookie.xml
new file mode 100644
index 000000000000..74dd134355e2
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/food_cookie.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <group>
+ <path
+ android:fillColor="#55FFFFFF"
+ android:fillType="evenOdd"
+ android:pathData="M5.71 18.29A8.99 8.99 0 0 0 22 13c0-3-1.46-5.65-3.71-7.29A8.99 8.99 0 0 0 2 11c0 3 1.46 5.65 3.71 7.29z"/>
+ <path
+ android:fillColor="#FFFFFFFF"
+ android:fillType="evenOdd"
+ android:pathData="M7.25 19.18A8.5 8.5 0 0 0 19.19 7.24 9 9 0 0 1 7.24 19.19z"/>
+ <path
+ android:fillColor="#55FFFFFF"
+ android:pathData="M10.5 3a0.5 0.5 0 1 1 1 0v2.05a0.5 0.5 0 1 1-1 0V3zm3.1 0.42a0.5 0.5 0 0 1 0.93 0.39l-0.8 1.88A0.5 0.5 0 1 1 12.8 5.3l0.8-1.88zm2.7 1.57a0.5 0.5 0 1 1 0.71 0.7l-1.45 1.46a0.5 0.5 0 0 1-0.7-0.71l1.44-1.45zm1.9 2.5a0.5 0.5 0 0 1 0.38 0.92l-1.9 0.77a0.5 0.5 0 0 1-0.37-0.93l1.9-0.77zM19 10.5a0.5 0.5 0 1 1 0 1h-2.05a0.5 0.5 0 0 1 0-1H19zm-0.42 3.1a0.5 0.5 0 0 1-0.39 0.93l-1.88-0.8a0.5 0.5 0 1 1 0.39-0.92l1.88 0.8zm-1.57 2.7a0.5 0.5 0 1 1-0.7 0.71l-1.46-1.45a0.5 0.5 0 0 1 0.71-0.7l1.45 1.44zm-2.5 1.9a0.5 0.5 0 1 1-0.92 0.38l-0.77-1.9a0.5 0.5 0 0 1 0.93-0.37l0.77 1.9zM11.5 19a0.5 0.5 0 1 1-1 0v-2.05a0.5 0.5 0 0 1 1 0V19zm-3.1-0.42a0.5 0.5 0 0 1-0.93-0.39l0.8-1.88A0.5 0.5 0 0 1 9.2 16.7l-0.8 1.88zm-2.7-1.57a0.5 0.5 0 1 1-0.71-0.7l1.45-1.46a0.5 0.5 0 0 1 0.7 0.71L5.7 17.01zm-1.9-2.48a0.5 0.5 0 0 1-0.38-0.92l1.88-0.8a0.5 0.5 0 0 1 0.4 0.92l-1.9 0.8zM3 11.5a0.5 0.5 0 1 1 0-1h2.05a0.5 0.5 0 1 1 0 1H3zm0.42-3.1A0.5 0.5 0 0 1 3.8 7.46l1.88 0.8A0.5 0.5 0 1 1 5.3 9.2L3.42 8.4zm1.57-2.7a0.5 0.5 0 1 1 0.7-0.71l1.46 1.45a0.5 0.5 0 0 1-0.71 0.7L4.99 5.7zm2.5-1.9A0.5 0.5 0 0 1 8.4 3.41l0.77 1.9a0.5 0.5 0 0 1-0.93 0.37L7.48 3.8z"/>
+ </group>
+</vector> \ No newline at end of file
diff --git a/packages/EasterEgg/res/drawable/food_dish.xml b/packages/EasterEgg/res/drawable/food_dish.xml
new file mode 100644
index 000000000000..3fff6a90fad2
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/food_dish.xml
@@ -0,0 +1,24 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M24,13.8C11.3,13.8,1,18.3,1,24c0,5.7,10.3,10.2,23,10.2S47,29.7,47,24C47,18.3,36.7,13.8,24,13.8z M33.7,26.6 c1.1,-0.6,1.8,-1.3,1.8,-2c0,-2.1,-5.2,-3.8,-11.7,-3.8s-11.7,1.7,-11.7,3.8c0,0.6,0.4,1.2,1.2,1.7c-1.7,-0.8,-2.8,-1.7,-2.8,-2.8 c0,-2.5,6,-4.5,13.4,-4.5s13.4,2,13.4,4.5C37.4,24.7,36,25.8,33.7,26.6z"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/food_donut.xml b/packages/EasterEgg/res/drawable/food_donut.xml
new file mode 100644
index 000000000000..eaf831ea560c
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/food_donut.xml
@@ -0,0 +1,24 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M24,4.5c-10.5,0,-19,8.5,-19,19s8.5,19,19,19s19,-8.5,19,-19S34.5,4.5,24,4.5z M35.2,15.5l1.6,-1.1 c0.3,-0.2,0.6,-0.1,0.8,0.1l0.1,0.1c0.2,0.3,0.1,0.6,-0.1,0.8l-1.6,1.1c-0.3,0.2,-0.6,0.1,-0.8,-0.1l-0.1,-0.1 C34.9,16.1,35,15.7,35.2,15.5z M32.7,10.7c0,-0.3,0.3,-0.5,0.6,-0.5l0.1,0c0.3,0,0.5,0.3,0.5,0.6l-0.2,2c0,0.3,-0.3,0.5,-0.6,0.5l-0.1,0 c-0.3,0,-0.5,-0.3,-0.5,-0.6L32.7,10.7z M31.7,15.1l1.5,-0.2c0.2,0,0.5,0.1,0.5,0.4l0,0.1c0,0.2,-0.1,0.5,-0.4,0.5l-1.5,0.2 c-0.2,0,-0.5,-0.1,-0.5,-0.4l0,-0.1C31.3,15.4,31.5,15.2,31.7,15.1z M28.8,10.6l1.6,-1.1c0.3,-0.2,0.6,-0.1,0.8,0.1l0.1,0.1 c0.2,0.3,0.1,0.6,-0.1,0.8l-1.6,1.1c-0.3,0.2,-0.6,0.1,-0.8,-0.1l-0.1,-0.1C28.4,11.1,28.5,10.8,28.8,10.6z M25.8,6 c0,-0.3,0.3,-0.5,0.6,-0.5l0.1,0c0.3,0,0.5,0.3,0.5,0.6l-0.2,2c0,0.3,-0.3,0.5,-0.6,0.5l-0.1,0c-0.3,0,-0.5,-0.3,-0.5,-0.6L25.8,6z M20.7,6.5l1.9,-0.7c0.3,-0.1,0.6,0,0.7,0.3l0,0.1c0.1,0.3,0,0.6,-0.3,0.7l-1.9,0.7c-0.3,0.1,-0.6,0,-0.7,-0.3l0,-0.1 C20.3,6.9,20.4,6.6,20.7,6.5z M19.9,10.9l1.5,-0.2c0.2,0,0.5,0.1,0.5,0.4l0,0.1c0,0.2,-0.1,0.5,-0.4,0.5l-1.5,0.2 c-0.2,0,-0.5,-0.1,-0.5,-0.4l0,-0.1C19.5,11.1,19.7,10.9,19.9,10.9z M16,10.9L16,10.9c0.2,-0.3,0.4,-0.4,0.6,-0.3l1.3,0.7 c0.2,0.1,0.3,0.4,0.2,0.6L18,12c-0.1,0.2,-0.4,0.3,-0.6,0.2l-1.3,-0.7C15.9,11.4,15.8,11.1,16,10.9z M15.8,18.5c0.2,0,0.4,0.1,0.5,0.4 l0,0.1c0,0.2,-0.1,0.4,-0.4,0.5l-1.5,0.2c-0.2,0,-0.4,-0.1,-0.5,-0.4l0,-0.1c0,-0.2,0.1,-0.4,0.4,-0.5L15.8,18.5z M14,21.8l-1.6,1.1 c-0.3,0.2,-0.6,0.1,-0.8,-0.1l-0.1,-0.1c-0.2,-0.3,-0.1,-0.6,0.1,-0.8l1.6,-1.1c0.3,-0.2,0.6,-0.1,0.8,0.1l0.1,0.1 C14.3,21.3,14.3,21.6,14,21.8z M12.4,12L12.4,12c0.3,-0.2,0.5,-0.2,0.7,-0.1l1,1.1c0.2,0.2,0.2,0.4,0,0.6L14,13.7 c-0.2,0.2,-0.4,0.2,-0.6,0l-1,-1.1C12.2,12.4,12.2,12.1,12.4,12z M8.3,24.5c0,0.3,-0.3,0.5,-0.6,0.5l-0.1,0c-0.3,0,-0.5,-0.3,-0.5,-0.6 l0.2,-2c0,-0.3,0.3,-0.5,0.6,-0.5l0.1,0c0.3,0,0.5,0.3,0.5,0.6L8.3,24.5z M8.5,16.2v-0.1c0,-0.3,0.2,-0.6,0.6,-0.6h2 c0.3,0,0.6,0.2,0.6,0.6v0.1c0,0.3,-0.2,0.6,-0.6,0.6H9C8.7,16.7,8.5,16.5,8.5,16.2z M10.3,20.7c-0.3,0.2,-0.6,0.1,-0.8,-0.1l-0.1,-0.1 c-0.2,-0.3,-0.1,-0.6,0.1,-0.8l1.6,-1.1c0.3,-0.2,0.6,-0.1,0.8,0.1l0.1,0.1c0.2,0.3,0.1,0.6,-0.1,0.8L10.3,20.7z M11.3,28.3l0,-0.1 c-0.1,-0.3,0,-0.6,0.3,-0.7l1.9,-0.7c0.3,-0.1,0.6,0,0.7,0.3l0,0.1c0.1,0.3,0,0.6,-0.3,0.7L12,28.6C11.7,28.7,11.4,28.6,11.3,28.3z M14.4,33c0,0.2,-0.2,0.4,-0.4,0.4h-1.5c-0.2,0,-0.4,-0.2,-0.4,-0.4v-0.1c0,-0.2,0.2,-0.4,0.4,-0.4H14c0.2,0,0.4,0.2,0.4,0.4V33z M17.9,35.2 l-1.6,1.1c-0.3,0.2,-0.6,0.1,-0.8,-0.1l-0.1,-0.1c-0.2,-0.3,-0.1,-0.6,0.1,-0.8l1.6,-1.1c0.3,-0.2,0.6,-0.1,0.8,0.1l0.1,0.1 C18.2,34.7,18.2,35.1,17.9,35.2z M20.7,33.8l-0.1,0.1c-0.1,0.3,-0.5,0.4,-0.8,0.2l-1.7,-1c-0.3,-0.1,-0.4,-0.5,-0.2,-0.8l0.1,-0.1 c0.1,-0.3,0.5,-0.4,0.8,-0.2l1.7,1C20.7,33.2,20.8,33.5,20.7,33.8z M17.5,23.5c0,-3.6,2.9,-6.5,6.5,-6.5s6.5,2.9,6.5,6.5 c0,3.6,-2.9,6.5,-6.5,6.5S17.5,27.1,17.5,23.5z M27.4,35.7l-1.9,0.7c-0.3,0.1,-0.6,0,-0.7,-0.3l0,-0.1c-0.1,-0.3,0,-0.6,0.3,-0.7l1.9,-0.7 c0.3,-0.1,0.6,0,0.7,0.3l0,0.1C27.9,35.3,27.7,35.6,27.4,35.7z M29.7,32.7l-1.4,0.5c-0.2,0.1,-0.5,0,-0.5,-0.3l0,-0.1 c-0.1,-0.2,0,-0.5,0.3,-0.5l1.4,-0.5c0.2,-0.1,0.5,0,0.5,0.3l0,0.1C30,32.3,29.9,32.6,29.7,32.7z M32.8,35.5l-0.1,0.1 c-0.1,0.3,-0.5,0.4,-0.8,0.2l-1.7,-1c-0.3,-0.1,-0.4,-0.5,-0.2,-0.8l0.1,-0.1c0.1,-0.3,0.5,-0.4,0.8,-0.2l1.7,1C32.8,34.9,32.9,35.2,32.8,35.5z M33.7,30.9c0,0.2,-0.2,0.4,-0.5,0.4l-0.1,0c-0.2,0,-0.4,-0.2,-0.4,-0.5l0.1,-1.5c0,-0.2,0.2,-0.4,0.5,-0.4l0.1,0c0.2,0,0.4,0.2,0.4,0.5 L33.7,30.9z M34.5,26.5l-1.3,0.9c-0.2,0.1,-0.5,0.1,-0.6,-0.1l-0.1,-0.1c-0.1,-0.2,-0.1,-0.5,0.1,-0.6l1.3,-0.9c0.2,-0.1,0.5,-0.1,0.6,0.1 l0.1,0.1C34.8,26.1,34.7,26.3,34.5,26.5z M35.6,20.6l-1.7,-1c-0.3,-0.1,-0.4,-0.5,-0.2,-0.8l0.1,-0.1c0.1,-0.3,0.5,-0.4,0.8,-0.2l1.7,1 c0.3,0.1,0.4,0.5,0.2,0.8l-0.1,0.1C36.2,20.6,35.8,20.7,35.6,20.6z M38.6,27.1l-1.6,1.1c-0.3,0.2,-0.6,0.1,-0.8,-0.1L36.1,28 c-0.2,-0.3,-0.1,-0.6,0.1,-0.8l1.6,-1.1c0.3,-0.2,0.6,-0.1,0.8,0.1l0.1,0.1C38.9,26.6,38.8,27,38.6,27.1z M39,19.4l-1.5,0.2 c-0.2,0,-0.5,-0.1,-0.5,-0.4l0,-0.1c0,-0.2,0.1,-0.5,0.4,-0.5l1.5,-0.2c0.2,0,0.5,0.1,0.5,0.4l0,0.1C39.4,19.1,39.2,19.3,39,19.4z"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/food_sysuituna.xml b/packages/EasterEgg/res/drawable/food_sysuituna.xml
new file mode 100644
index 000000000000..28cf4a2c7683
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/food_sysuituna.xml
@@ -0,0 +1,24 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M46,18.4l-5.8,4.6c-3.9,-3.2,-8.9,-5.6,-14.6,-6.3l1.2,-6l-7.3,5.9C12.5,17.2,6.4,20,2,24.3l7.2,1.4L2,27 c4.3,4.2,10.4,7.1,17.3,7.6l3.1,2.5L22,34.8c7.1,0,13.5,-2.5,18.2,-6.5l5.8,4.6l-1.4,-7.2L46,18.4z M14.3,24.8l-0.6,0.6l-1.1,-1.1 l-1.1,1.1l-0.6,-0.6l1.1,-1.1l-1.1,-1.1l0.6,-0.6l1.1,1.1l1.1,-1.1l0.6,0.6l-1.1,1.1L14.3,24.8z M18.8,29.1c0.7,-0.8,1.1,-2.2,1.1,-3.8 c0,-1.6,-0.4,-3,-1.1,-3.8c1.1,0.5,1.9,2,1.9,3.8S19.9,28.5,18.8,29.1z M20.7,29.1c0.7,-0.8,1.1,-2.2,1.1,-3.8c0,-1.6,-0.4,-3,-1.1,-3.8 c1.1,0.5,1.9,2,1.9,3.8S21.8,28.5,20.7,29.1z"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/foot1.xml b/packages/EasterEgg/res/drawable/foot1.xml
new file mode 100644
index 000000000000..0d9085998a18
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/foot1.xml
@@ -0,0 +1,22 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path android:name="foot1" android:fillColor="#FF000000" android:pathData="M11.5,43m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/foot2.xml b/packages/EasterEgg/res/drawable/foot2.xml
new file mode 100644
index 000000000000..364ba0cd861c
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/foot2.xml
@@ -0,0 +1,22 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path android:name="foot2" android:fillColor="#FF000000" android:pathData="M18.5,43m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/foot3.xml b/packages/EasterEgg/res/drawable/foot3.xml
new file mode 100644
index 000000000000..e3a512a2568d
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/foot3.xml
@@ -0,0 +1,22 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path android:name="foot3" android:fillColor="#FF000000" android:pathData="M29.5,43m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/foot4.xml b/packages/EasterEgg/res/drawable/foot4.xml
new file mode 100644
index 000000000000..66b78fa26649
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/foot4.xml
@@ -0,0 +1,22 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path android:name="foot4" android:fillColor="#FF000000" android:pathData="M36.5,43m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/head.xml b/packages/EasterEgg/res/drawable/head.xml
new file mode 100644
index 000000000000..df600a8613cd
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/head.xml
@@ -0,0 +1,22 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path android:name="head" android:fillColor="#FF000000" android:pathData="M9,18.5c0,-8.3 6.8,-15 15,-15s15,6.7 15,15H9z"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/ic_bowl.xml b/packages/EasterEgg/res/drawable/ic_bowl.xml
new file mode 100644
index 000000000000..d55565d92988
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_bowl.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M3,19L21,19"
+ android:strokeWidth="2"
+ android:strokeColor="#FF8000"/>
+ <path
+ android:pathData="M7,12L4.5,19H19.5L17,12H7Z"
+ android:strokeLineJoin="round"
+ android:strokeWidth="2"
+ android:strokeColor="#FF8000"/>
+ <path
+ android:strokeWidth="1"
+ android:pathData="M7.5257,18.8419L9.5257,12.8419"
+ android:strokeColor="#FF8000"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/ic_close.xml b/packages/EasterEgg/res/drawable/ic_close.xml
new file mode 100644
index 000000000000..60ea36b11fcc
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_close.xml
@@ -0,0 +1,24 @@
+<!--
+ Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24.0dp"
+ android:height="24.0dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M19.0,6.41L17.59,5.0 12.0,10.59 6.41,5.0 5.0,6.41 10.59,12.0 5.0,17.59 6.41,19.0 12.0,13.41 17.59,19.0 19.0,17.59 13.41,12.0z"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/ic_foodbowl_filled.xml b/packages/EasterEgg/res/drawable/ic_foodbowl_filled.xml
new file mode 100644
index 000000000000..54961af68aef
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_foodbowl_filled.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M3,19L21,19"
+ android:strokeWidth="2"
+ android:strokeColor="#FF8000"/>
+ <path
+ android:pathData="M9,9m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
+ android:fillColor="#FF8000"/>
+ <path
+ android:pathData="M12,9m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
+ android:fillColor="#FF8000"/>
+ <path
+ android:pathData="M15,9m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
+ android:fillColor="#FF8000"/>
+ <path
+ android:pathData="M13.5,7m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
+ android:fillColor="#FF8000"/>
+ <path
+ android:pathData="M10.5,7m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
+ android:fillColor="#FF8000"/>
+ <path
+ android:pathData="M6.0583,11.6637C6.2004,11.2657 6.5774,11 7,11H17C17.4226,11 17.7996,11.2657 17.9418,11.6637L19.8476,17H4.1524L6.0583,11.6637ZM7.5,12L6,16H7L8.5,12H7.5Z"
+ android:fillColor="#FF8000"
+ android:fillType="evenOdd"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/ic_fullcat_icon.xml b/packages/EasterEgg/res/drawable/ic_fullcat_icon.xml
new file mode 100644
index 000000000000..5dca3d18f2d4
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_fullcat_icon.xml
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+ <path
+ android:pathData="M15.38,1.02l5.12,5.32l-6.32,2.72l1.2,-8.04z"
+ android:fillColor="#808080"/>
+ <path
+ android:pathData="M32.63,1.02l-5.13,5.32l6.32,2.72l-1.19,-8.04z"
+ android:fillColor="#808080"/>
+ <path
+ android:pathData="M33.82,9.06l-4.77,-1.82l3.58,-6.22l1.19,8.04z"
+ android:fillColor="#666"/>
+ <path
+ android:pathData="M15.38,1.02l3.57,6.22l-4.77,1.82l1.2,-8.04z"
+ android:fillColor="#666"/>
+ <path
+ android:pathData="M9,18.5a15,15 0,0 1,30 0Z"
+ android:fillColor="#808080"/>
+ <path
+ android:pathData="M19.5,15.25a4.5,3.25 0,1 0,9 0a4.5,3.25 0,1 0,-9 0z"
+ android:fillColor="#fff"/>
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M20.5,11c0,1.73 -3,1.73 -3,0S20.5,9.35 20.5,11Z"/>
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M30.5,11c0,1.73 -3,1.73 -3,0S30.5,9.35 30.5,11Z"/>
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M25.15,13c0,1.28 -2.3,1.28 -2.3,0S25.15,11.73 25.15,13Z"/>
+ <path
+ android:pathData="M29,14.29a2.78,2.78 0,0 1,-2.33 1.41A2.75,2.75 0,0 1,24 13"
+ android:strokeWidth="1.25"
+ android:fillColor="#00000000"
+ android:strokeColor="#000"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M24,13a2.66,2.66 0,0 1,-2.67 2.69A2.53,2.53 0,0 1,19 14.29"
+ android:strokeWidth="1.25"
+ android:fillColor="#00000000"
+ android:strokeColor="#000"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M9,20h30v18h-30z"
+ android:fillColor="#808080"/>
+ <path
+ android:pathData="M11.5,43m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0"
+ android:fillColor="#fff"/>
+ <path
+ android:pathData="M9,37h5v6h-5z"
+ android:fillColor="#808080"/>
+ <path
+ android:pathData="M29.5,43m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0"
+ android:fillColor="#fff"/>
+ <path
+ android:pathData="M27,37h5v6h-5z"
+ android:fillColor="#808080"/>
+ <path
+ android:pathData="M36.5,43m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0"
+ android:fillColor="#fff"/>
+ <path
+ android:pathData="M34,37h5v6h-5z"
+ android:fillColor="#808080"/>
+ <path
+ android:pathData="M18.5,43m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0"
+ android:fillColor="#fff"/>
+ <path
+ android:pathData="M16,37h5v6h-5z"
+ android:fillColor="#808080"/>
+ <path
+ android:pathData="M35,35.5h5.9a3.8,3.8 0,0 0,3.8 -3.8V25.5"
+ android:strokeWidth="5"
+ android:fillColor="#00000000"
+ android:strokeColor="#808080"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M40,38l0,-5l-1,0l0,5l1,0z"
+ android:fillColor="#666"/>
+ <path
+ android:pathData="M20.5,25A6.47,6.47 0,0 0,14 31.5V38H27V31.5A6.47,6.47 0,0 0,20.5 25Z"
+ android:fillColor="#fff"/>
+ <path
+ android:pathData="M16,38h5v1h-5z"
+ android:fillColor="#666"/>
+ <path
+ android:pathData="M9,18.5h30v1.5h-30z"
+ android:fillColor="#3ddc84"/>
+ <path
+ android:pathData="M29,16.75l-10,5l0,-5l10,5l0,-5z"
+ android:fillColor="#3ddc84"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/ic_share.xml b/packages/EasterEgg/res/drawable/ic_share.xml
new file mode 100644
index 000000000000..8cebc7ed46de
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_share.xml
@@ -0,0 +1,24 @@
+<!--
+ Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24.0dp"
+ android:height="24.0dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M18.0,16.08c-0.76,0.0 -1.4,0.3 -1.9,0.77L8.91,12.7c0.05,-0.2 0.09,-0.4 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.5,0.5 1.2,0.81 2.0,0.81 1.66,0.0 3.0,-1.34 3.0,-3.0s-1.34,-3.0 -3.0,-3.0 -3.0,1.34 -3.0,3.0c0.0,0.2 0.0,0.4 0.0,0.7L8.04,9.81C7.5,9.31 6.79,9.0 6.0,9.0c-1.66,0.0 -3.0,1.34 -3.0,3.0s1.34,3.0 3.0,3.0c0.79,0.0 1.5,-0.31 2.04,-0.81l7.12,4.16c0.0,0.21 0.0,0.43 0.0,0.65 0.0,1.61 1.31,2.92 2.92,2.92 1.61,0.0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/ic_toy_ball.xml b/packages/EasterEgg/res/drawable/ic_toy_ball.xml
new file mode 100644
index 000000000000..411084b2a272
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_toy_ball.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M12,12m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0"
+ android:strokeWidth="2"
+ android:fillColor="#00000000"
+ android:strokeColor="#FF4080"/>
+ <path
+ android:pathData="M12,9C12.5523,9 13,8.5523 13,8C13,7.4477 12.5523,7 12,7V9ZM7,12C7,12.5523 7.4477,13 8,13C8.5523,13 9,12.5523 9,12H7ZM12,7C10.6748,7 9.4332,7.6526 8.5429,8.5429C7.6526,9.4332 7,10.6748 7,12H9C9,11.3252 9.3475,10.5668 9.9571,9.9571C10.5668,9.3475 11.3252,9 12,9V7Z"
+ android:fillColor="#FF4080"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/ic_toy_fish.xml b/packages/EasterEgg/res/drawable/ic_toy_fish.xml
new file mode 100644
index 000000000000..bb01e9f32bfb
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_toy_fish.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M14.8492,8.9498C15.7483,9.8489 16.132,11.201 15.9095,12.7581C15.6871,14.3155 14.8589,16.0111 13.435,17.435C12.0111,18.8589 10.3155,19.6871 8.7581,19.9096C7.201,20.132 5.8488,19.7484 4.9497,18.8493C4.0506,17.9501 3.667,16.598 3.8894,15.0409C4.1119,13.4835 4.9401,11.7879 6.364,10.364C7.7879,8.9401 9.4835,8.1119 11.0409,7.8895C12.598,7.667 13.9501,8.0506 14.8492,8.9498Z"
+ android:strokeWidth="2"
+ android:fillColor="#00000000"
+ android:strokeColor="#FF4080"/>
+ <path
+ android:pathData="M7,15m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
+ android:fillColor="#FF4080"/>
+ <path
+ android:pathData="M14.5,8L17.5,3C17.5,3 18,4.5 19,6C20,7.5 21.5,8.5 21.5,8.5L16.5,10"
+ android:strokeLineJoin="round"
+ android:strokeWidth="2"
+ android:fillColor="#00000000"
+ android:strokeColor="#FF4080"/>
+ <path
+ android:pathData="M8.5,4.5L6.5,10L10,7.5L8.5,4.5Z"
+ android:strokeLineJoin="round"
+ android:strokeWidth="2"
+ android:fillColor="#FF4080"
+ android:strokeColor="#FF4080"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/ic_toy_laser.xml b/packages/EasterEgg/res/drawable/ic_toy_laser.xml
new file mode 100644
index 000000000000..8fe84ffbd38c
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_toy_laser.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M12.866,3.5C12.6874,3.1906 12.3573,3 12,3C11.6427,3 11.3126,3.1906 11.134,3.5L2.4737,18.5C2.2951,18.8094 2.2951,19.1906 2.4737,19.5C2.6523,19.8094 2.9825,20 3.3398,20H20.6603C21.0175,20 21.3476,19.8094 21.5263,19.5C21.7049,19.1906 21.7049,18.8094 21.5263,18.5L12.866,3.5Z"
+ android:strokeLineJoin="round"
+ android:strokeWidth="2"
+ android:fillColor="#00000000"
+ android:strokeColor="#FF4080"/>
+ <path
+ android:pathData="M8,13.5h11v1h-11z"
+ android:fillColor="#FF4080"/>
+ <path
+ android:pathData="M11.5,10h1v8h-1z"
+ android:fillColor="#FF4080"/>
+ <path
+ android:pathData="M8.86,11.4883l0.6283,-0.6283l5.6547,5.6547l-0.6283,0.6283z"
+ android:fillColor="#FF4080"/>
+ <path
+ android:pathData="M9.4883,17.143l-0.6283,-0.6283l5.6547,-5.6547l0.6283,0.6283z"
+ android:fillColor="#FF4080"/>
+ <path
+ android:pathData="M12,14m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"
+ android:fillColor="#FF4080"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/ic_toy_mouse.xml b/packages/EasterEgg/res/drawable/ic_toy_mouse.xml
new file mode 100644
index 000000000000..ba3dc3322083
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_toy_mouse.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M14.8492,8.9498C15.7483,9.8489 16.132,11.201 15.9095,12.7581C15.6871,14.3155 14.8589,16.0111 13.435,17.435C12.0111,18.8589 10.3155,19.6871 8.7581,19.9096C7.201,20.132 5.8488,19.7484 4.9497,18.8493C4.0506,17.9501 3.667,16.598 3.8894,15.0409C4.1119,13.4835 4.9401,11.7879 6.364,10.364C7.7879,8.9401 9.4835,8.1119 11.0409,7.8895C12.598,7.667 13.9501,8.0506 14.8492,8.9498Z"
+ android:strokeWidth="2"
+ android:fillColor="#00000000"
+ android:strokeColor="#FF4080"/>
+ <path
+ android:pathData="M3.5,11.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0"
+ android:strokeWidth="2"
+ android:fillColor="#00000000"
+ android:strokeColor="#FF4080"/>
+ <path
+ android:pathData="M7.5,7.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0"
+ android:strokeWidth="2"
+ android:fillColor="#00000000"
+ android:strokeColor="#FF4080"/>
+ <path
+ android:pathData="M7,15m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
+ android:fillColor="#FF4080"/>
+ <path
+ android:pathData="M9,13m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
+ android:fillColor="#FF4080"/>
+ <path
+ android:pathData="M22,4C22,3.4477 21.5523,3 21,3C20.4477,3 20,3.4477 20,4L22,4ZM15,9C14.873,9.9919 14.8735,9.992 14.874,9.992C14.8742,9.9921 14.8747,9.9921 14.8751,9.9922C14.8759,9.9923 14.8768,9.9924 14.8778,9.9925C14.8798,9.9928 14.8821,9.993 14.8848,9.9934C14.8902,9.994 14.8971,9.9948 14.9054,9.9958C14.922,9.9976 14.9442,10 14.9718,10.0026C15.027,10.0079 15.1036,10.0143 15.1985,10.02C15.3881,10.0312 15.6534,10.0396 15.9697,10.0294C16.5957,10.0092 17.455,9.9156 18.3326,9.6062C19.2147,9.2951 20.1482,8.7534 20.8583,7.8203C21.5743,6.8795 22,5.6234 22,4L20,4C20,5.2607 19.6757,6.0717 19.2667,6.6091C18.8518,7.1543 18.2853,7.5021 17.6674,7.72C17.045,7.9395 16.4043,8.0144 15.9053,8.0304C15.6591,8.0384 15.4556,8.0317 15.3171,8.0235C15.248,8.0194 15.1957,8.0149 15.1629,8.0118C15.1466,8.0102 15.1352,8.009 15.129,8.0083C15.126,8.008 15.1242,8.0077 15.1239,8.0077C15.1237,8.0077 15.1239,8.0077 15.1244,8.0078C15.1247,8.0078 15.125,8.0078 15.1254,8.0079C15.1256,8.0079 15.126,8.008 15.1262,8.008C15.1266,8.008 15.127,8.0081 15,9Z"
+ android:fillColor="#FF4080"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/ic_water.xml b/packages/EasterEgg/res/drawable/ic_water.xml
new file mode 100644
index 000000000000..7d94b2409636
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_water.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M17.654,7.563L12,2L6.346,7.563C5.6036,8.2877 5.0136,9.1533 4.6108,10.1094C4.2079,11.0654 4.0002,12.0924 4,13.1299C4,15.2516 4.8429,17.2863 6.3432,18.7866C7.8434,20.2869 9.8783,21.1299 12,21.1299C14.1217,21.1299 16.1566,20.2869 17.6569,18.7866C19.1572,17.2863 20,15.2516 20,13.1299C20,12.0924 19.7925,11.0654 19.3896,10.1094C18.9867,9.1533 18.3966,8.2875 17.654,7.563V7.563ZM12,19C10.4265,19.0152 8.9113,18.4056 7.7865,17.3052C6.6617,16.2048 6.0192,14.7033 6,13.1299C5.9996,12.3577 6.1541,11.5933 6.4543,10.8818C6.7546,10.1704 7.1945,9.5262 7.748,8.9878L12,4.8061L16.252,8.9888C16.8056,9.5269 17.2456,10.171 17.5458,10.8823C17.8461,11.5936 18.0005,12.3578 18,13.1299C17.9807,14.7033 17.3383,16.2048 16.2135,17.3052C15.0887,18.4056 13.5735,19.0152 12,19Z"
+ android:fillColor="#0080FF"/>
+ <path
+ android:pathData="M16,12C15.7348,12 15.4804,12.1054 15.2929,12.293C15.1054,12.4805 15,12.7348 15,13C15,13.7956 14.6839,14.5585 14.1213,15.1211C13.5587,15.6837 12.7956,16 12,16C11.7348,16 11.4804,16.1054 11.2929,16.293C11.1054,16.4805 11,16.7348 11,17C11,17.2652 11.1054,17.5195 11.2929,17.707C11.4804,17.8946 11.7348,18 12,18C13.3256,17.9984 14.5964,17.471 15.5338,16.5337C16.4711,15.5964 16.9984,14.3256 17,13C17,12.7348 16.8946,12.4805 16.7071,12.293C16.5196,12.1054 16.2652,12 16,12Z"
+ android:fillColor="#0080FF"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/ic_water_filled.xml b/packages/EasterEgg/res/drawable/ic_water_filled.xml
new file mode 100644
index 000000000000..eed171d05668
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_water_filled.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M17.654,7.563L12,2L6.346,7.563C5.6036,8.2877 5.0136,9.1533 4.6108,10.1094C4.2079,11.0654 4.0002,12.0924 4,13.1299C4.0174,15.2343 4.87,17.2458 6.3703,18.7217C7.8705,20.1975 9.8956,21.017 12,21C14.1044,21.017 16.1295,20.1975 17.6297,18.7217C19.13,17.2458 19.9826,15.2343 20,13.1299C20,12.0924 19.7925,11.0654 19.3896,10.1094C18.9867,9.1533 18.3966,8.2875 17.654,7.563V7.563ZM12,18C11.7348,18 11.4804,17.8946 11.2929,17.707C11.1054,17.5195 11,17.2652 11,17C11,16.7348 11.1054,16.4805 11.2929,16.293C11.4804,16.1054 11.7348,16 12,16C12.7956,16 13.5587,15.6837 14.1213,15.1211C14.6839,14.5585 15,13.7956 15,13C15,12.7348 15.1054,12.4805 15.2929,12.293C15.4804,12.1054 15.7348,12 16,12C16.2652,12 16.5196,12.1054 16.7071,12.293C16.8946,12.4805 17,12.7348 17,13C16.9984,14.3256 16.4711,15.5964 15.5338,16.5337C14.5964,17.471 13.3256,17.9984 12,18Z"
+ android:fillColor="#0080FF"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/ic_waterbowl_filled.xml b/packages/EasterEgg/res/drawable/ic_waterbowl_filled.xml
new file mode 100644
index 000000000000..28b1fa824060
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_waterbowl_filled.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M3,19L21,19"
+ android:strokeWidth="2"
+ android:fillColor="#00000000"
+ android:strokeColor="#000000"/>
+ <path
+ android:pathData="M6.0583,11.6637C6.2004,11.2657 6.5774,11 7,11H17C17.4226,11 17.7996,11.2657 17.9418,11.6637L19.8476,17H4.1524L6.0583,11.6637ZM7.5,12L6,16H7L8.5,12H7.5Z"
+ android:fillColor="#000000"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M13.4135,6.3907L12,5L10.5865,6.3907C10.4009,6.5719 10.2534,6.7883 10.1527,7.0273C10.052,7.2663 10.0001,7.5231 10,7.7825C10.0044,8.3086 10.2175,8.8115 10.5926,9.1804C10.9676,9.5494 11.4739,9.7543 12,9.75C12.5261,9.7543 13.0324,9.5494 13.4074,9.1804C13.7825,8.8115 13.9956,8.3086 14,7.7825C14,7.5231 13.9481,7.2664 13.8474,7.0273C13.7467,6.7883 13.5991,6.5719 13.4135,6.3907V6.3907ZM12,9C11.9337,9 11.8701,8.9736 11.8232,8.9268C11.7763,8.8799 11.75,8.8163 11.75,8.75C11.75,8.6837 11.7763,8.6201 11.8232,8.5732C11.8701,8.5264 11.9337,8.5 12,8.5C12.1989,8.5 12.3897,8.4209 12.5303,8.2803C12.671,8.1396 12.75,7.9489 12.75,7.75C12.75,7.6837 12.7763,7.6201 12.8232,7.5732C12.8701,7.5264 12.9337,7.5 13,7.5C13.0663,7.5 13.1299,7.5264 13.1768,7.5732C13.2237,7.6201 13.25,7.6837 13.25,7.75C13.2496,8.0814 13.1178,8.3991 12.8834,8.6334C12.6491,8.8678 12.3314,8.9996 12,9Z"
+ android:fillColor="#000000"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/icon.xml b/packages/EasterEgg/res/drawable/icon.xml
new file mode 100644
index 000000000000..7f8d4fa8833f
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/icon.xml
@@ -0,0 +1,19 @@
+<!--
+Copyright (C) 2018 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/icon_bg"/>
+ <foreground android:drawable="@drawable/android_11_dial"/>
+</adaptive-icon>
diff --git a/packages/EasterEgg/res/drawable/icon_bg.xml b/packages/EasterEgg/res/drawable/icon_bg.xml
index 659f98be4f43..31b2a7f9a333 100644
--- a/packages/EasterEgg/res/drawable/icon_bg.xml
+++ b/packages/EasterEgg/res/drawable/icon_bg.xml
@@ -1,8 +1,7 @@
-<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright (C) 2018 The Android Open Source Project
+Copyright (C) 2018 The Android Open Source Project
- Licensed under the Apache License, Version 2.0 (the "License");
+ Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
@@ -15,4 +14,5 @@
limitations under the License.
-->
<color xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/q_clue_text" />
+ android:color="#073042" />
+
diff --git a/packages/EasterEgg/res/drawable/left_ear.xml b/packages/EasterEgg/res/drawable/left_ear.xml
new file mode 100644
index 000000000000..2b98736df039
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/left_ear.xml
@@ -0,0 +1,22 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path android:name="left_ear" android:fillColor="#FF000000" android:pathData="M15.4,1l5.1000004,5.3l-6.3,2.8000002z"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/left_ear_inside.xml b/packages/EasterEgg/res/drawable/left_ear_inside.xml
new file mode 100644
index 000000000000..1d947edc31e2
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/left_ear_inside.xml
@@ -0,0 +1,22 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path android:name="left_ear_inside" android:fillColor="#FF000000" android:pathData="M15.4,1l3.5,6.2l-4.7,1.9z"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/left_eye.xml b/packages/EasterEgg/res/drawable/left_eye.xml
new file mode 100644
index 000000000000..4dde1b661393
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/left_eye.xml
@@ -0,0 +1,22 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path android:name="left_eye" android:fillColor="#FF000000" android:pathData="M20.5,11c0,1.7 -3,1.7 -3,0C17.5,9.3 20.5,9.3 20.5,11z"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/leg1.xml b/packages/EasterEgg/res/drawable/leg1.xml
new file mode 100644
index 000000000000..d72c746b6232
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/leg1.xml
@@ -0,0 +1,22 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path android:name="leg1" android:fillColor="#FF000000" android:pathData="M9,37h5v6h-5z"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/leg2.xml b/packages/EasterEgg/res/drawable/leg2.xml
new file mode 100644
index 000000000000..a772a870af7d
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/leg2.xml
@@ -0,0 +1,22 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path android:name="leg2" android:fillColor="#FF000000" android:pathData="M16,37h5v6h-5z"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/leg2_shadow.xml b/packages/EasterEgg/res/drawable/leg2_shadow.xml
new file mode 100644
index 000000000000..b01bd6995c0b
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/leg2_shadow.xml
@@ -0,0 +1,22 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path android:name="leg2_shadow" android:fillColor="#FF000000" android:pathData="M16,37h5v3h-5z"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/leg3.xml b/packages/EasterEgg/res/drawable/leg3.xml
new file mode 100644
index 000000000000..d471236687b5
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/leg3.xml
@@ -0,0 +1,22 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path android:name="leg3" android:fillColor="#FF000000" android:pathData="M27,37h5v6h-5z"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/leg4.xml b/packages/EasterEgg/res/drawable/leg4.xml
new file mode 100644
index 000000000000..e5868eb80c59
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/leg4.xml
@@ -0,0 +1,22 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path android:name="leg4" android:fillColor="#FF000000" android:pathData="M34,37h5v6h-5z"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/mouth.xml b/packages/EasterEgg/res/drawable/mouth.xml
new file mode 100644
index 000000000000..ddcf2e82f976
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/mouth.xml
@@ -0,0 +1,27 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path android:name="mouth"
+ android:strokeColor="#FF000000"
+ android:strokeWidth="1.2"
+ android:strokeLineCap="round"
+ android:pathData="M29,14.3c-0.4,0.8 -1.3,1.4 -2.3,1.4c-1.4,0 -2.7,-1.3 -2.7,-2.7
+ M24,13c0,1.5 -1.2,2.7 -2.7,2.7c-1,0 -1.9,-0.5 -2.3,-1.4"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/nose.xml b/packages/EasterEgg/res/drawable/nose.xml
new file mode 100644
index 000000000000..d403cd1baadf
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/nose.xml
@@ -0,0 +1,22 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path android:name="nose" android:fillColor="#FF000000" android:pathData="M25.2,13c0,1.3 -2.3,1.3 -2.3,0S25.2,11.7 25.2,13z"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/octo_bg.xml b/packages/EasterEgg/res/drawable/octo_bg.xml
new file mode 100644
index 000000000000..1e46cf434a8b
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/octo_bg.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <gradient android:angle="-90"
+ android:startColor="#FF205090"
+ android:endColor="#FF001040"
+ android:type="linear"
+ />
+</shape> \ No newline at end of file
diff --git a/packages/EasterEgg/res/drawable/right_ear.xml b/packages/EasterEgg/res/drawable/right_ear.xml
new file mode 100644
index 000000000000..b9fb4d1c7470
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/right_ear.xml
@@ -0,0 +1,22 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path android:name="right_ear" android:fillColor="#FF000000" android:pathData="M32.6,1l-5.0999985,5.3l6.299999,2.8000002z"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/right_ear_inside.xml b/packages/EasterEgg/res/drawable/right_ear_inside.xml
new file mode 100644
index 000000000000..86b6e3428d1f
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/right_ear_inside.xml
@@ -0,0 +1,23 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+
+ <path android:name="right_ear_inside" android:fillColor="#FF000000" android:pathData="M33.8,9.1l-4.7,-1.9l3.5,-6.2z"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/right_eye.xml b/packages/EasterEgg/res/drawable/right_eye.xml
new file mode 100644
index 000000000000..a1871a62c25b
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/right_eye.xml
@@ -0,0 +1,22 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path android:name="right_eye" android:fillColor="#FF000000" android:pathData="M30.5,11c0,1.7 -3,1.7 -3,0C27.5,9.3 30.5,9.3 30.5,11z"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/stat_icon.xml b/packages/EasterEgg/res/drawable/stat_icon.xml
new file mode 100644
index 000000000000..608cb2017c3f
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/stat_icon.xml
@@ -0,0 +1,30 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M12,2C6.5,2 2,6.5 2,12c0,5.5 4.5,10 10,10s10,-4.5 10,-10C22,6.5 17.5,2 12,2zM5.5,11c0,-1.6 3,-1.6 3,0C8.5,12.7 5.5,12.7 5.5,11zM17.5,14.6c-0.6,1 -1.7,1.7 -2.9,1.7c-1.1,0 -2,-0.6 -2.6,-1.4c-0.6,0.9 -1.6,1.4 -2.7,1.4c-1.3,0 -2.3,-0.7 -2.9,-1.8c-0.2,-0.3 0,-0.7 0.3,-0.8c0.3,-0.2 0.7,0 0.8,0.3c0.3,0.7 1,1.1 1.8,1.1c0.9,0 1.6,-0.5 1.9,-1.3c-0.2,-0.2 -0.4,-0.4 -0.4,-0.7c0,-1.3 2.3,-1.3 2.3,0c0,0.3 -0.2,0.6 -0.4,0.7c0.3,0.8 1.1,1.3 1.9,1.3c0.8,0 1.5,-0.6 1.8,-1.1c0.2,-0.3 0.6,-0.4 0.9,-0.2C17.6,13.9 17.7,14.3 17.5,14.6zM15.5,11c0,-1.6 3,-1.6 3,0C18.5,12.7 15.5,12.7 15.5,11z"/>
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M5.2,1.0l4.1000004,4.2l-5.0,2.1000004z"/>
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M18.8,1.0l-4.0999994,4.2l5.000001,2.1000004z"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/tail.xml b/packages/EasterEgg/res/drawable/tail.xml
new file mode 100644
index 000000000000..0cca23c3e16c
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/tail.xml
@@ -0,0 +1,26 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path android:name="tail"
+ android:strokeColor="#FF000000"
+ android:strokeWidth="5"
+ android:strokeLineCap="round"
+ android:pathData="M35,35.5h5.9c2.1,0 3.8,-1.7 3.8,-3.8v-6.2"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/tail_cap.xml b/packages/EasterEgg/res/drawable/tail_cap.xml
new file mode 100644
index 000000000000..b82f6f9b478a
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/tail_cap.xml
@@ -0,0 +1,22 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path android:name="tail_cap" android:fillColor="#FF000000" android:pathData="M42.2,25.5c0,-1.4 1.1,-2.5 2.5,-2.5s2.5,1.1 2.5,2.5H42.2z"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/tail_shadow.xml b/packages/EasterEgg/res/drawable/tail_shadow.xml
new file mode 100644
index 000000000000..bb1ff12b3afe
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/tail_shadow.xml
@@ -0,0 +1,22 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path android:name="tail_shadow" android:fillColor="#FF000000" android:pathData="M40,38l0,-5l-1,0l0,5z"/>
+</vector>
diff --git a/packages/EasterEgg/res/layout/activity_paint.xml b/packages/EasterEgg/res/layout/activity_paint.xml
index a4c17afd1531..8e916b021bbd 100644
--- a/packages/EasterEgg/res/layout/activity_paint.xml
+++ b/packages/EasterEgg/res/layout/activity_paint.xml
@@ -16,7 +16,7 @@
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
- xmlns:app="http://schemas.android.com/apk/res/com.android.egg"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#666"
@@ -45,4 +45,4 @@
/>
-</FrameLayout> \ No newline at end of file
+</FrameLayout>
diff --git a/packages/EasterEgg/res/layout/cat_view.xml b/packages/EasterEgg/res/layout/cat_view.xml
new file mode 100644
index 000000000000..85b494d2e68d
--- /dev/null
+++ b/packages/EasterEgg/res/layout/cat_view.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ except in compliance with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software distributed under the
+ License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the specific language governing
+ permissions and limitations under the License.
+ -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ android:gravity="center_horizontal"
+ android:clipToPadding="false">
+
+ <FrameLayout
+ android:layout_width="96dp"
+ android:layout_height="wrap_content">
+
+ <ImageView
+ android:id="@android:id/icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="10dp"
+ android:layout_gravity="center"
+ android:scaleType="fitCenter" />
+
+ <LinearLayout
+ android:id="@+id/contextGroup"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="invisible"
+ android:layout_gravity="bottom">
+
+ <ImageView
+ android:id="@android:id/shareText"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:padding="8dp"
+ android:src="@drawable/ic_share"
+ android:scaleType="fitCenter"
+ android:background="#40000000"/>
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_weight="1" />
+
+ <ImageView
+ android:id="@android:id/closeButton"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:padding="4dp"
+ android:src="@drawable/ic_close"
+ android:scaleType="fitCenter"
+ android:background="#40000000"/>
+
+ </LinearLayout>
+
+ </FrameLayout>
+
+ <TextView
+ android:id="@android:id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceListItem"
+ android:gravity="center"/>
+</LinearLayout>
+
diff --git a/packages/EasterEgg/res/layout/edit_text.xml b/packages/EasterEgg/res/layout/edit_text.xml
new file mode 100644
index 000000000000..9f7ac802bad4
--- /dev/null
+++ b/packages/EasterEgg/res/layout/edit_text.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ except in compliance with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software distributed under the
+ License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the specific language governing
+ permissions and limitations under the License.
+ -->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingStart="20dp"
+ android:paddingEnd="20dp">
+
+ <EditText
+ android:id="@android:id/edit"
+ android:maxLines="1"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+</FrameLayout> \ No newline at end of file
diff --git a/packages/EasterEgg/res/layout/food_layout.xml b/packages/EasterEgg/res/layout/food_layout.xml
new file mode 100644
index 000000000000..d0ca0c8899aa
--- /dev/null
+++ b/packages/EasterEgg/res/layout/food_layout.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ except in compliance with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software distributed under the
+ License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the specific language governing
+ permissions and limitations under the License.
+ -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ android:paddingLeft="4dp" android:paddingRight="4dp"
+ android:paddingBottom="6dp" android:paddingTop="6dp">
+ <ImageView
+ android:layout_width="64dp"
+ android:layout_height="64dp"
+ android:id="@+id/icon"
+ android:tint="?android:attr/colorControlNormal"/>
+ <TextView android:layout_width="64dp" android:layout_height="wrap_content"
+ android:gravity="top|center_horizontal"
+ android:id="@+id/text" />
+</LinearLayout>
diff --git a/packages/EasterEgg/res/layout/neko_activity.xml b/packages/EasterEgg/res/layout/neko_activity.xml
new file mode 100644
index 000000000000..c258137ca710
--- /dev/null
+++ b/packages/EasterEgg/res/layout/neko_activity.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/holder"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"/>
+</FrameLayout> \ No newline at end of file
diff --git a/packages/EasterEgg/res/values/cat_strings.xml b/packages/EasterEgg/res/values/cat_strings.xml
new file mode 100644
index 000000000000..5214fc1ab01d
--- /dev/null
+++ b/packages/EasterEgg/res/values/cat_strings.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="notification_name" translatable="false">Android Neko</string>
+ <string name="notification_channel_name" translatable="false">New cats</string>
+ <string name="default_tile_name" translatable="false">\????</string>
+ <string name="notification_title" translatable="false">A cat is here.</string>
+ <string name="default_cat_name" translatable="false">Cat #%s</string>
+ <string name="directory_name" translatable="false">Cats</string>
+ <string name="confirm_delete" translatable="false">Forget %s?</string>
+ <string-array name="food_names" translatable="false">
+ <item>Empty dish</item>
+ <item>Bits</item>
+ <item>Fish</item>
+ <item>Chicken</item>
+ <item>Treat</item>
+ </string-array>
+ <array name="food_icons">
+ <item>@drawable/food_dish</item>
+ <item>@drawable/food_bits</item>
+ <item>@drawable/food_sysuituna</item>
+ <item>@drawable/food_chicken</item>
+ <item>@drawable/food_cookie</item>
+ </array>
+ <integer-array name="food_intervals">
+ <item>0</item>
+ <item>15</item>
+ <item>30</item>
+ <item>60</item>
+ <item>120</item>
+ </integer-array>
+ <integer-array name="food_new_cat_prob">
+ <item>0</item>
+ <item>5</item>
+ <item>35</item>
+ <item>65</item>
+ <item>90</item>
+ </integer-array>
+ <string-array name="cat_messages" translatable="false">
+ <item>😸</item>
+ <item>😹</item>
+ <item>😺</item>
+ <item>😻</item>
+ <item>😼</item>
+ <item>😽</item>
+ <item>😾</item>
+ <item>😿</item>
+ <item>🙀</item>
+ <item>💩</item>
+ <item>🐁</item>
+ </string-array>
+ <string-array name="rare_cat_messages" translatable="false">
+ <item>🍩</item>
+ <item>🍭</item>
+ <item>🍫</item>
+ <item>🍨</item>
+ <item>🔔</item>
+ <item>🐝</item>
+ <item>🍪</item>
+ <item>🥧</item>
+ </string-array>
+ <string name="control_toy_title" translatable="false">Toy</string>
+ <string name="control_toy_subtitle" translatable="false">Tap to use</string>
+ <string name="control_toy_status" translatable="false">Cat attracted!</string>
+ <string name="control_water_title" translatable="false">Water bubbler</string>
+ <string name="control_water_subtitle" translatable="false">Swipe to fill</string>
+ <string name="control_food_title" translatable="false">Food bowl</string>
+ <string name="control_food_subtitle" translatable="false">Tap to refill</string>
+ <string name="control_food_status_full" translatable="false">Full</string>
+ <string name="control_food_status_empty" translatable="false">Empty</string>
+</resources>
+
diff --git a/packages/EasterEgg/res/values/dimens.xml b/packages/EasterEgg/res/values/dimens.xml
new file mode 100644
index 000000000000..e9dcebd27f7b
--- /dev/null
+++ b/packages/EasterEgg/res/values/dimens.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <dimen name="neko_display_size">64dp</dimen>
+</resources>
diff --git a/packages/EasterEgg/res/values/strings.xml b/packages/EasterEgg/res/values/strings.xml
index b95ec6be4c84..25f94215d433 100644
--- a/packages/EasterEgg/res/values/strings.xml
+++ b/packages/EasterEgg/res/values/strings.xml
@@ -14,11 +14,13 @@ Copyright (C) 2018 The Android Open Source Project
limitations under the License.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android">
- <string name="app_name" translatable="false">Android Q Easter Egg</string>
+ <string name="app_name" translatable="false">Android R Easter Egg</string>
<!-- name of the Q easter egg, a nonogram-style icon puzzle -->
<string name="q_egg_name" translatable="false">Icon Quiz</string>
<!-- name of the P easter egg, a humble paint program -->
<string name="p_egg_name" translatable="false">PAINT.APK</string>
+
+ <string name="r_egg_name" translatable="false">Cat Controls</string>
</resources>
diff --git a/packages/EasterEgg/res/xml/filepaths.xml b/packages/EasterEgg/res/xml/filepaths.xml
new file mode 100644
index 000000000000..2130025e9265
--- /dev/null
+++ b/packages/EasterEgg/res/xml/filepaths.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<paths>
+ <external-path name="cats" path="Pictures/Cats" />
+</paths> \ No newline at end of file
diff --git a/packages/EasterEgg/src/com/android/egg/neko/Cat.java b/packages/EasterEgg/src/com/android/egg/neko/Cat.java
new file mode 100644
index 000000000000..cd59a735068b
--- /dev/null
+++ b/packages/EasterEgg/src/com/android/egg/neko/Cat.java
@@ -0,0 +1,524 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.egg.neko;
+
+import static com.android.egg.neko.NekoLand.CHAN_ID;
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.app.Person;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+
+import com.android.egg.R;
+import com.android.internal.logging.MetricsLogger;
+
+import java.io.ByteArrayOutputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.ThreadLocalRandom;
+
+/** It's a cat. */
+public class Cat extends Drawable {
+ public static final long[] PURR = {0, 40, 20, 40, 20, 40, 20, 40, 20, 40, 20, 40};
+
+ public static final boolean ALL_CATS_IN_ONE_CONVERSATION = true;
+
+ public static final String GLOBAL_SHORTCUT_ID = "com.android.egg.neko:allcats";
+ public static final String SHORTCUT_ID_PREFIX = "com.android.egg.neko:cat:";
+
+ private Random mNotSoRandom;
+ private Bitmap mBitmap;
+ private long mSeed;
+ private String mName;
+ private int mBodyColor;
+ private int mFootType;
+ private boolean mBowTie;
+ private String mFirstMessage;
+
+ private synchronized Random notSoRandom(long seed) {
+ if (mNotSoRandom == null) {
+ mNotSoRandom = new Random();
+ mNotSoRandom.setSeed(seed);
+ }
+ return mNotSoRandom;
+ }
+
+ public static final float frandrange(Random r, float a, float b) {
+ return (b - a) * r.nextFloat() + a;
+ }
+
+ public static final Object choose(Random r, Object... l) {
+ return l[r.nextInt(l.length)];
+ }
+
+ public static final int chooseP(Random r, int[] a) {
+ return chooseP(r, a, 1000);
+ }
+
+ public static final int chooseP(Random r, int[] a, int sum) {
+ int pct = r.nextInt(sum);
+ final int stop = a.length - 2;
+ int i = 0;
+ while (i < stop) {
+ pct -= a[i];
+ if (pct < 0) break;
+ i += 2;
+ }
+ return a[i + 1];
+ }
+
+ public static final int getColorIndex(int q, int[] a) {
+ for (int i = 1; i < a.length; i += 2) {
+ if (a[i] == q) {
+ return i / 2;
+ }
+ }
+ return -1;
+ }
+
+ public static final int[] P_BODY_COLORS = {
+ 180, 0xFF212121, // black
+ 180, 0xFFFFFFFF, // white
+ 140, 0xFF616161, // gray
+ 140, 0xFF795548, // brown
+ 100, 0xFF90A4AE, // steel
+ 100, 0xFFFFF9C4, // buff
+ 100, 0xFFFF8F00, // orange
+ 5, 0xFF29B6F6, // blue..?
+ 5, 0xFFFFCDD2, // pink!?
+ 5, 0xFFCE93D8, // purple?!?!?
+ 4, 0xFF43A047, // yeah, why not green
+ 1, 0, // ?!?!?!
+ };
+
+ public static final int[] P_COLLAR_COLORS = {
+ 250, 0xFFFFFFFF,
+ 250, 0xFF000000,
+ 250, 0xFFF44336,
+ 50, 0xFF1976D2,
+ 50, 0xFFFDD835,
+ 50, 0xFFFB8C00,
+ 50, 0xFFF48FB1,
+ 50, 0xFF4CAF50,
+ };
+
+ public static final int[] P_BELLY_COLORS = {
+ 750, 0,
+ 250, 0xFFFFFFFF,
+ };
+
+ public static final int[] P_DARK_SPOT_COLORS = {
+ 700, 0,
+ 250, 0xFF212121,
+ 50, 0xFF6D4C41,
+ };
+
+ public static final int[] P_LIGHT_SPOT_COLORS = {
+ 700, 0,
+ 300, 0xFFFFFFFF,
+ };
+
+ private CatParts D;
+
+ public static void tint(int color, Drawable... ds) {
+ for (Drawable d : ds) {
+ if (d != null) {
+ d.mutate().setTint(color);
+ }
+ }
+ }
+
+ public static boolean isDark(int color) {
+ final int r = (color & 0xFF0000) >> 16;
+ final int g = (color & 0x00FF00) >> 8;
+ final int b = color & 0x0000FF;
+ return (r + g + b) < 0x80;
+ }
+
+ public Cat(Context context, long seed) {
+ D = new CatParts(context);
+ mSeed = seed;
+
+ setName(context.getString(R.string.default_cat_name,
+ String.valueOf(mSeed % 1000)));
+
+ final Random nsr = notSoRandom(seed);
+
+ // body color
+ mBodyColor = chooseP(nsr, P_BODY_COLORS);
+ if (mBodyColor == 0) mBodyColor = Color.HSVToColor(new float[]{
+ nsr.nextFloat() * 360f, frandrange(nsr, 0.5f, 1f), frandrange(nsr, 0.5f, 1f)});
+
+ tint(mBodyColor, D.body, D.head, D.leg1, D.leg2, D.leg3, D.leg4, D.tail,
+ D.leftEar, D.rightEar, D.foot1, D.foot2, D.foot3, D.foot4, D.tailCap);
+ tint(0x20000000, D.leg2Shadow, D.tailShadow);
+ if (isDark(mBodyColor)) {
+ tint(0xFFFFFFFF, D.leftEye, D.rightEye, D.mouth, D.nose);
+ }
+ tint(isDark(mBodyColor) ? 0xFFEF9A9A : 0x20D50000, D.leftEarInside, D.rightEarInside);
+
+ tint(chooseP(nsr, P_BELLY_COLORS), D.belly);
+ tint(chooseP(nsr, P_BELLY_COLORS), D.back);
+ final int faceColor = chooseP(nsr, P_BELLY_COLORS);
+ tint(faceColor, D.faceSpot);
+ if (!isDark(faceColor)) {
+ tint(0xFF000000, D.mouth, D.nose);
+ }
+
+ mFootType = 0;
+ if (nsr.nextFloat() < 0.25f) {
+ mFootType = 4;
+ tint(0xFFFFFFFF, D.foot1, D.foot2, D.foot3, D.foot4);
+ } else {
+ if (nsr.nextFloat() < 0.25f) {
+ mFootType = 2;
+ tint(0xFFFFFFFF, D.foot1, D.foot3);
+ } else if (nsr.nextFloat() < 0.25f) {
+ mFootType = 3; // maybe -2 would be better? meh.
+ tint(0xFFFFFFFF, D.foot2, D.foot4);
+ } else if (nsr.nextFloat() < 0.1f) {
+ mFootType = 1;
+ tint(0xFFFFFFFF, (Drawable) choose(nsr, D.foot1, D.foot2, D.foot3, D.foot4));
+ }
+ }
+
+ tint(nsr.nextFloat() < 0.333f ? 0xFFFFFFFF : mBodyColor, D.tailCap);
+
+ final int capColor = chooseP(nsr, isDark(mBodyColor) ? P_LIGHT_SPOT_COLORS : P_DARK_SPOT_COLORS);
+ tint(capColor, D.cap);
+ //tint(chooseP(nsr, isDark(bodyColor) ? P_LIGHT_SPOT_COLORS : P_DARK_SPOT_COLORS), D.nose);
+
+ final int collarColor = chooseP(nsr, P_COLLAR_COLORS);
+ tint(collarColor, D.collar);
+ mBowTie = nsr.nextFloat() < 0.1f;
+ tint(mBowTie ? collarColor : 0, D.bowtie);
+
+ String[] messages = context.getResources().getStringArray(
+ nsr.nextFloat() < 0.1f ? R.array.rare_cat_messages : R.array.cat_messages);
+ mFirstMessage = (String) choose(nsr, (Object[]) messages);
+ if (nsr.nextFloat() < 0.5f) mFirstMessage = mFirstMessage + mFirstMessage + mFirstMessage;
+ }
+
+ public static Cat fromShortcutId(Context context, String shortcutId) {
+ if (shortcutId.startsWith(SHORTCUT_ID_PREFIX)) {
+ return new Cat(context, Long.parseLong(shortcutId.replace(SHORTCUT_ID_PREFIX, "")));
+ }
+ return null;
+ }
+
+ public static Cat create(Context context) {
+ return new Cat(context, Math.abs(ThreadLocalRandom.current().nextInt()));
+ }
+
+ public Notification.Builder buildNotification(Context context) {
+ final Bundle extras = new Bundle();
+ extras.putString("android.substName", context.getString(R.string.notification_name));
+
+ final Icon notificationIcon = createNotificationLargeIcon(context);
+
+ final Intent intent = new Intent(Intent.ACTION_MAIN)
+ .setClass(context, NekoLand.class)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ ShortcutInfo shortcut = new ShortcutInfo.Builder(context, getShortcutId())
+ .setActivity(intent.getComponent())
+ .setIntent(intent)
+ .setShortLabel(getName())
+ .setIcon(createShortcutIcon(context))
+ .setLongLived(true)
+ .build();
+ context.getSystemService(ShortcutManager.class).addDynamicShortcuts(List.of(shortcut));
+
+ Notification.BubbleMetadata bubbs = new Notification.BubbleMetadata.Builder()
+ .setIntent(
+ PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE))
+ .setIcon(notificationIcon)
+ .setSuppressNotification(false)
+ .setDesiredHeight(context.getResources().getDisplayMetrics().heightPixels)
+ .build();
+
+ return new Notification.Builder(context, CHAN_ID)
+ .setSmallIcon(Icon.createWithResource(context, R.drawable.stat_icon))
+ .setLargeIcon(notificationIcon)
+ .setColor(getBodyColor())
+ .setContentTitle(context.getString(R.string.notification_title))
+ .setShowWhen(true)
+ .setCategory(Notification.CATEGORY_STATUS)
+ .setContentText(getName())
+ .setContentIntent(
+ PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE))
+ .setAutoCancel(true)
+ .setStyle(new Notification.MessagingStyle(createPerson())
+ .addMessage(mFirstMessage, System.currentTimeMillis(), createPerson())
+ .setConversationTitle(getName())
+ )
+ .setBubbleMetadata(bubbs)
+ .setShortcutId(getShortcutId())
+ .addExtras(extras);
+ }
+
+ private Person createPerson() {
+ return new Person.Builder()
+ .setName(getName())
+ .setBot(true)
+ .setKey(getShortcutId())
+ .build();
+ }
+
+ public long getSeed() {
+ return mSeed;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ final int w = Math.min(canvas.getWidth(), canvas.getHeight());
+ final int h = w;
+
+ if (mBitmap == null || mBitmap.getWidth() != w || mBitmap.getHeight() != h) {
+ mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
+ final Canvas bitCanvas = new Canvas(mBitmap);
+ slowDraw(bitCanvas, 0, 0, w, h);
+ }
+ canvas.drawBitmap(mBitmap, 0, 0, null);
+ }
+
+ private void slowDraw(Canvas canvas, int x, int y, int w, int h) {
+ for (int i = 0; i < D.drawingOrder.length; i++) {
+ final Drawable d = D.drawingOrder[i];
+ if (d != null) {
+ d.setBounds(x, y, x + w, y + h);
+ d.draw(canvas);
+ }
+ }
+
+ }
+
+ public Bitmap createBitmap(int w, int h) {
+ if (mBitmap != null && mBitmap.getWidth() == w && mBitmap.getHeight() == h) {
+ return mBitmap.copy(mBitmap.getConfig(), true);
+ }
+ Bitmap result = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
+ slowDraw(new Canvas(result), 0, 0, w, h);
+ return result;
+ }
+
+ public static Icon recompressIcon(Icon bitmapIcon) {
+ if (bitmapIcon.getType() != Icon.TYPE_BITMAP) return bitmapIcon;
+ try {
+ final Bitmap bits = (Bitmap) Icon.class.getDeclaredMethod("getBitmap").invoke(bitmapIcon);
+ final ByteArrayOutputStream ostream = new ByteArrayOutputStream(
+ bits.getWidth() * bits.getHeight() * 2); // guess 50% compression
+ final boolean ok = bits.compress(Bitmap.CompressFormat.PNG, 100, ostream);
+ if (!ok) return null;
+ return Icon.createWithData(ostream.toByteArray(), 0, ostream.size());
+ } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) {
+ return bitmapIcon;
+ }
+ }
+
+ public Icon createNotificationLargeIcon(Context context) {
+ final Resources res = context.getResources();
+ final int w = res.getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
+ final int h = res.getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
+ return recompressIcon(createIcon(context, w, h));
+ }
+
+ public Icon createShortcutIcon(Context context) {
+ // shortcuts do not support compressed bitmaps
+ final Resources res = context.getResources();
+ final int w = res.getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
+ final int h = res.getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
+ return createIcon(context, w, h);
+ }
+
+ public Icon createIcon(Context context, int w, int h) {
+ Bitmap result = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
+ final Canvas canvas = new Canvas(result);
+ float[] hsv = new float[3];
+ Color.colorToHSV(mBodyColor, hsv);
+ hsv[2] = (hsv[2] > 0.5f)
+ ? (hsv[2] - 0.25f)
+ : (hsv[2] + 0.25f);
+ //final Paint pt = new Paint();
+ //pt.setColor(Color.HSVToColor(hsv));
+ //float r = w/2;
+ //canvas.drawCircle(r, r, r, pt);
+ // int m = w/10;
+
+ // Adaptive bitmaps!
+ canvas.drawColor(Color.HSVToColor(hsv));
+ int m = w / 4;
+
+ slowDraw(canvas, m, m, w - m - m, h - m - m);
+
+ return Icon.createWithAdaptiveBitmap(result);
+ }
+
+ @Override
+ public void setAlpha(int i) {
+
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter colorFilter) {
+
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public void setName(String name) {
+ this.mName = name;
+ }
+
+ public int getBodyColor() {
+ return mBodyColor;
+ }
+
+ public void logAdd(Context context) {
+ logCatAction(context, "egg_neko_add");
+ }
+
+ public void logRename(Context context) {
+ logCatAction(context, "egg_neko_rename");
+ }
+
+ public void logRemove(Context context) {
+ logCatAction(context, "egg_neko_remove");
+ }
+
+ public void logShare(Context context) {
+ logCatAction(context, "egg_neko_share");
+ }
+
+ private void logCatAction(Context context, String prefix) {
+ MetricsLogger.count(context, prefix, 1);
+ MetricsLogger.histogram(context, prefix + "_color",
+ getColorIndex(mBodyColor, P_BODY_COLORS));
+ MetricsLogger.histogram(context, prefix + "_bowtie", mBowTie ? 1 : 0);
+ MetricsLogger.histogram(context, prefix + "_feet", mFootType);
+ }
+
+ public String getShortcutId() {
+ return ALL_CATS_IN_ONE_CONVERSATION
+ ? GLOBAL_SHORTCUT_ID
+ : (SHORTCUT_ID_PREFIX + mSeed);
+ }
+
+ public static class CatParts {
+ public Drawable leftEar;
+ public Drawable rightEar;
+ public Drawable rightEarInside;
+ public Drawable leftEarInside;
+ public Drawable head;
+ public Drawable faceSpot;
+ public Drawable cap;
+ public Drawable mouth;
+ public Drawable body;
+ public Drawable foot1;
+ public Drawable leg1;
+ public Drawable foot2;
+ public Drawable leg2;
+ public Drawable foot3;
+ public Drawable leg3;
+ public Drawable foot4;
+ public Drawable leg4;
+ public Drawable tail;
+ public Drawable leg2Shadow;
+ public Drawable tailShadow;
+ public Drawable tailCap;
+ public Drawable belly;
+ public Drawable back;
+ public Drawable rightEye;
+ public Drawable leftEye;
+ public Drawable nose;
+ public Drawable bowtie;
+ public Drawable collar;
+ public Drawable[] drawingOrder;
+
+ public CatParts(Context context) {
+ body = context.getDrawable(R.drawable.body);
+ head = context.getDrawable(R.drawable.head);
+ leg1 = context.getDrawable(R.drawable.leg1);
+ leg2 = context.getDrawable(R.drawable.leg2);
+ leg3 = context.getDrawable(R.drawable.leg3);
+ leg4 = context.getDrawable(R.drawable.leg4);
+ tail = context.getDrawable(R.drawable.tail);
+ leftEar = context.getDrawable(R.drawable.left_ear);
+ rightEar = context.getDrawable(R.drawable.right_ear);
+ rightEarInside = context.getDrawable(R.drawable.right_ear_inside);
+ leftEarInside = context.getDrawable(R.drawable.left_ear_inside);
+ faceSpot = context.getDrawable(R.drawable.face_spot);
+ cap = context.getDrawable(R.drawable.cap);
+ mouth = context.getDrawable(R.drawable.mouth);
+ foot4 = context.getDrawable(R.drawable.foot4);
+ foot3 = context.getDrawable(R.drawable.foot3);
+ foot1 = context.getDrawable(R.drawable.foot1);
+ foot2 = context.getDrawable(R.drawable.foot2);
+ leg2Shadow = context.getDrawable(R.drawable.leg2_shadow);
+ tailShadow = context.getDrawable(R.drawable.tail_shadow);
+ tailCap = context.getDrawable(R.drawable.tail_cap);
+ belly = context.getDrawable(R.drawable.belly);
+ back = context.getDrawable(R.drawable.back);
+ rightEye = context.getDrawable(R.drawable.right_eye);
+ leftEye = context.getDrawable(R.drawable.left_eye);
+ nose = context.getDrawable(R.drawable.nose);
+ collar = context.getDrawable(R.drawable.collar);
+ bowtie = context.getDrawable(R.drawable.bowtie);
+ drawingOrder = getDrawingOrder();
+ }
+
+ private Drawable[] getDrawingOrder() {
+ return new Drawable[]{
+ collar,
+ leftEar, leftEarInside, rightEar, rightEarInside,
+ head,
+ faceSpot,
+ cap,
+ leftEye, rightEye,
+ nose, mouth,
+ tail, tailCap, tailShadow,
+ foot1, leg1,
+ foot2, leg2,
+ foot3, leg3,
+ foot4, leg4,
+ leg2Shadow,
+ body, belly,
+ bowtie
+ };
+ }
+ }
+}
diff --git a/packages/EasterEgg/src/com/android/egg/neko/Food.java b/packages/EasterEgg/src/com/android/egg/neko/Food.java
new file mode 100644
index 000000000000..aeffc4adfd3a
--- /dev/null
+++ b/packages/EasterEgg/src/com/android/egg/neko/Food.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.egg.neko;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Icon;
+
+import com.android.egg.R;
+
+public class Food {
+ private final int mType;
+
+ private static int[] sIcons;
+ private static String[] sNames;
+
+ public Food(int type) {
+ mType = type;
+ }
+
+ public Icon getIcon(Context context) {
+ if (sIcons == null) {
+ TypedArray icons = context.getResources().obtainTypedArray(R.array.food_icons);
+ sIcons = new int[icons.length()];
+ for (int i = 0; i < sIcons.length; i++) {
+ sIcons[i] = icons.getResourceId(i, 0);
+ }
+ icons.recycle();
+ }
+ return Icon.createWithResource(context, sIcons[mType]);
+ }
+
+ public String getName(Context context) {
+ if (sNames == null) {
+ sNames = context.getResources().getStringArray(R.array.food_names);
+ }
+ return sNames[mType];
+ }
+
+ public long getInterval(Context context) {
+ return context.getResources().getIntArray(R.array.food_intervals)[mType];
+ }
+
+ public int getType() {
+ return mType;
+ }
+}
diff --git a/packages/EasterEgg/src/com/android/egg/neko/NekoActivationActivity.java b/packages/EasterEgg/src/com/android/egg/neko/NekoActivationActivity.java
new file mode 100644
index 000000000000..df461c6878f0
--- /dev/null
+++ b/packages/EasterEgg/src/com/android/egg/neko/NekoActivationActivity.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.egg.neko;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.pm.PackageManager;
+import android.provider.Settings;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.android.internal.logging.MetricsLogger;
+
+public class NekoActivationActivity extends Activity {
+ private static final String R_EGG_UNLOCK_SETTING = "egg_mode_r";
+
+ private void toastUp(String s) {
+ Toast toast = Toast.makeText(this, s, Toast.LENGTH_SHORT);
+ toast.show();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ final PackageManager pm = getPackageManager();
+ final ComponentName cn = new ComponentName(this, NekoControlsService.class);
+ final boolean componentEnabled = pm.getComponentEnabledSetting(cn)
+ == PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
+ if (Settings.System.getLong(getContentResolver(),
+ R_EGG_UNLOCK_SETTING, 0) == 0) {
+ if (componentEnabled) {
+ Log.v("Neko", "Disabling controls.");
+ pm.setComponentEnabledSetting(cn, PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+ PackageManager.DONT_KILL_APP);
+ MetricsLogger.histogram(this, "egg_neko_enable", 0);
+ toastUp("\uD83D\uDEAB");
+ } else {
+ Log.v("Neko", "Controls already disabled.");
+ }
+ } else {
+ if (!componentEnabled) {
+ Log.v("Neko", "Enabling controls.");
+ pm.setComponentEnabledSetting(cn, PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
+ PackageManager.DONT_KILL_APP);
+ MetricsLogger.histogram(this, "egg_neko_enable", 1);
+ toastUp("\uD83D\uDC31");
+ } else {
+ Log.v("Neko", "Controls already enabled.");
+ }
+ }
+ finish();
+ }
+}
diff --git a/packages/EasterEgg/src/com/android/egg/neko/NekoControlsService.kt b/packages/EasterEgg/src/com/android/egg/neko/NekoControlsService.kt
new file mode 100644
index 000000000000..56f599a3a219
--- /dev/null
+++ b/packages/EasterEgg/src/com/android/egg/neko/NekoControlsService.kt
@@ -0,0 +1,323 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.egg.neko
+
+import android.app.PendingIntent
+import android.content.Intent
+import android.content.res.ColorStateList
+import android.graphics.drawable.Icon
+import android.service.controls.Control
+import android.service.controls.ControlsProviderService
+import android.service.controls.DeviceTypes
+import android.service.controls.actions.ControlAction
+import android.service.controls.actions.FloatAction
+import android.service.controls.templates.ControlButton
+import android.service.controls.templates.RangeTemplate
+import android.service.controls.templates.StatelessTemplate
+import android.service.controls.templates.ToggleTemplate
+import android.text.SpannableStringBuilder
+import android.text.style.ForegroundColorSpan
+import android.util.Log
+import androidx.annotation.RequiresApi
+import com.android.internal.logging.MetricsLogger
+import java.util.Random
+import java.util.concurrent.Flow
+import java.util.function.Consumer
+
+import com.android.egg.R
+
+const val CONTROL_ID_WATER = "water"
+const val CONTROL_ID_FOOD = "food"
+const val CONTROL_ID_TOY = "toy"
+
+const val FOOD_SPAWN_CAT_DELAY_MINS = 5L
+
+const val COLOR_FOOD_FG = 0xFFFF8000.toInt()
+const val COLOR_FOOD_BG = COLOR_FOOD_FG and 0x40FFFFFF.toInt()
+const val COLOR_WATER_FG = 0xFF0080FF.toInt()
+const val COLOR_WATER_BG = COLOR_WATER_FG and 0x40FFFFFF.toInt()
+const val COLOR_TOY_FG = 0xFFFF4080.toInt()
+const val COLOR_TOY_BG = COLOR_TOY_FG and 0x40FFFFFF.toInt()
+
+val P_TOY_ICONS = intArrayOf(
+ 1, R.drawable.ic_toy_mouse,
+ 1, R.drawable.ic_toy_fish,
+ 1, R.drawable.ic_toy_ball,
+ 1, R.drawable.ic_toy_laser
+)
+
+@RequiresApi(30)
+fun Control_toString(control: Control): String {
+ val hc = String.format("0x%08x", control.hashCode())
+ return ("Control($hc id=${control.controlId}, type=${control.deviceType}, " +
+ "title=${control.title}, template=${control.controlTemplate})")
+}
+
+@RequiresApi(30)
+public class NekoControlsService : ControlsProviderService(), PrefState.PrefsListener {
+ private val TAG = "NekoControls"
+
+ private val controls = HashMap<String, Control>()
+ private val publishers = ArrayList<UglyPublisher>()
+ private val rng = Random()
+
+ private var lastToyIcon: Icon? = null
+
+ private lateinit var prefs: PrefState
+
+ override fun onCreate() {
+ super.onCreate()
+
+ prefs = PrefState(this)
+ prefs.setListener(this)
+
+ createDefaultControls()
+ }
+
+ override fun onPrefsChanged() {
+ createDefaultControls()
+ }
+
+ private fun createDefaultControls() {
+ val foodState: Int = prefs.foodState
+ if (foodState != 0) {
+ NekoService.registerJobIfNeeded(this, FOOD_SPAWN_CAT_DELAY_MINS)
+ }
+
+ val water = prefs.waterState
+
+ controls[CONTROL_ID_WATER] = makeWaterBowlControl(water)
+ controls[CONTROL_ID_FOOD] = makeFoodBowlControl(foodState != 0)
+ controls[CONTROL_ID_TOY] = makeToyControl(currentToyIcon(), false)
+ }
+
+ private fun currentToyIcon(): Icon {
+ val icon = lastToyIcon ?: randomToyIcon()
+ lastToyIcon = icon
+ return icon
+ }
+
+ private fun randomToyIcon(): Icon {
+ return Icon.createWithResource(resources, Cat.chooseP(rng, P_TOY_ICONS, 4))
+ }
+
+ private fun colorize(s: CharSequence, color: Int): CharSequence {
+ val ssb = SpannableStringBuilder(s)
+ ssb.setSpan(ForegroundColorSpan(color), 0, s.length, 0)
+ return ssb
+ }
+
+ private fun makeToyControl(icon: Icon?, thrown: Boolean): Control {
+ return Control.StatefulBuilder(CONTROL_ID_TOY, getPendingIntent())
+ .setDeviceType(DeviceTypes.TYPE_UNKNOWN)
+ .setCustomIcon(icon)
+ // ?.setTint(COLOR_TOY_FG)) // TODO(b/159559045): uncomment when fixed
+ .setCustomColor(ColorStateList.valueOf(COLOR_TOY_BG))
+ .setTitle(colorize(getString(R.string.control_toy_title), COLOR_TOY_FG))
+ .setStatusText(colorize(
+ if (thrown) getString(R.string.control_toy_status) else "",
+ COLOR_TOY_FG))
+ .setControlTemplate(StatelessTemplate("toy"))
+ .setStatus(Control.STATUS_OK)
+ .setSubtitle(if (thrown) "" else getString(R.string.control_toy_subtitle))
+ .setAppIntent(getAppIntent())
+ .build()
+ }
+
+ private fun makeWaterBowlControl(fillLevel: Float): Control {
+ return Control.StatefulBuilder(CONTROL_ID_WATER, getPendingIntent())
+ .setDeviceType(DeviceTypes.TYPE_KETTLE)
+ .setTitle(colorize(getString(R.string.control_water_title), COLOR_WATER_FG))
+ .setCustomColor(ColorStateList.valueOf(COLOR_WATER_BG))
+ .setCustomIcon(Icon.createWithResource(resources,
+ if (fillLevel >= 100f) R.drawable.ic_water_filled else R.drawable.ic_water))
+ //.setTint(COLOR_WATER_FG)) // TODO(b/159559045): uncomment when fixed
+ .setControlTemplate(RangeTemplate("waterlevel", 0f, 200f, fillLevel, 10f,
+ "%.0f mL"))
+ .setStatus(Control.STATUS_OK)
+ .setSubtitle(if (fillLevel == 0f) getString(R.string.control_water_subtitle) else "")
+ .build()
+ }
+
+ private fun makeFoodBowlControl(filled: Boolean): Control {
+ return Control.StatefulBuilder(CONTROL_ID_FOOD, getPendingIntent())
+ .setDeviceType(DeviceTypes.TYPE_UNKNOWN)
+ .setCustomColor(ColorStateList.valueOf(COLOR_FOOD_BG))
+ .setTitle(colorize(getString(R.string.control_food_title), COLOR_FOOD_FG))
+ .setCustomIcon(Icon.createWithResource(resources,
+ if (filled) R.drawable.ic_foodbowl_filled else R.drawable.ic_bowl))
+ // .setTint(COLOR_FOOD_FG)) // TODO(b/159559045): uncomment when fixed
+ .setStatusText(
+ if (filled) colorize(
+ getString(R.string.control_food_status_full), 0xCCFFFFFF.toInt())
+ else colorize(
+ getString(R.string.control_food_status_empty), 0x80FFFFFF.toInt()))
+ .setControlTemplate(ToggleTemplate("foodbowl", ControlButton(filled, "Refill")))
+ .setStatus(Control.STATUS_OK)
+ .setSubtitle(if (filled) "" else getString(R.string.control_food_subtitle))
+ .build()
+ }
+
+ private fun getPendingIntent(): PendingIntent {
+ val intent = Intent(Intent.ACTION_MAIN)
+ .setClass(this, NekoLand::class.java)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ return PendingIntent.getActivity(this, 0, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+ }
+
+ private fun getAppIntent(): PendingIntent {
+ return getPendingIntent()
+ }
+
+
+ override fun performControlAction(
+ controlId: String,
+ action: ControlAction,
+ consumer: Consumer<Int>
+ ) {
+ when (controlId) {
+ CONTROL_ID_FOOD -> {
+ // refill bowl
+ controls[CONTROL_ID_FOOD] = makeFoodBowlControl(true)
+ Log.v(TAG, "Bowl refilled. (Registering job.)")
+ NekoService.registerJob(this, FOOD_SPAWN_CAT_DELAY_MINS)
+ MetricsLogger.histogram(this, "egg_neko_offered_food", 11)
+ prefs.foodState = 11
+ }
+ CONTROL_ID_TOY -> {
+ Log.v(TAG, "Toy tossed.")
+ controls[CONTROL_ID_TOY] =
+ makeToyControl(currentToyIcon(), true)
+ // TODO: re-enable toy
+ Thread() {
+ Thread.sleep((1 + Random().nextInt(4)) * 1000L)
+ NekoService.getExistingCat(prefs)?.let {
+ NekoService.notifyCat(this, it)
+ }
+ controls[CONTROL_ID_TOY] = makeToyControl(randomToyIcon(), false)
+ pushControlChanges()
+ }.start()
+ }
+ CONTROL_ID_WATER -> {
+ if (action is FloatAction) {
+ controls[CONTROL_ID_WATER] = makeWaterBowlControl(action.newValue)
+ Log.v(TAG, "Water level set to " + action.newValue)
+ prefs.waterState = action.newValue
+ }
+ }
+ else -> {
+ return
+ }
+ }
+ consumer.accept(ControlAction.RESPONSE_OK)
+ pushControlChanges()
+ }
+
+ private fun pushControlChanges() {
+ Thread() {
+ publishers.forEach { it.refresh() }
+ }.start()
+ }
+
+ private fun makeStateless(c: Control?): Control? {
+ if (c == null) return null
+ return Control.StatelessBuilder(c.controlId, c.appIntent)
+ .setTitle(c.title)
+ .setSubtitle(c.subtitle)
+ .setStructure(c.structure)
+ .setDeviceType(c.deviceType)
+ .setCustomIcon(c.customIcon)
+ .setCustomColor(c.customColor)
+ .build()
+ }
+
+ override fun createPublisherFor(list: MutableList<String>): Flow.Publisher<Control> {
+ createDefaultControls()
+
+ val publisher = UglyPublisher(list, true)
+ publishers.add(publisher)
+ return publisher
+ }
+
+ override fun createPublisherForAllAvailable(): Flow.Publisher<Control> {
+ createDefaultControls()
+
+ val publisher = UglyPublisher(controls.keys, false)
+ publishers.add(publisher)
+ return publisher
+ }
+
+ private inner class UglyPublisher(
+ val controlKeys: Iterable<String>,
+ val indefinite: Boolean
+ ) : Flow.Publisher<Control> {
+ val subscriptions = ArrayList<UglySubscription>()
+
+ private inner class UglySubscription(
+ val initialControls: Iterator<Control>,
+ var subscriber: Flow.Subscriber<in Control>?
+ ) : Flow.Subscription {
+ override fun cancel() {
+ Log.v(TAG, "cancel subscription: $this for subscriber: $subscriber " +
+ "to publisher: $this@UglyPublisher")
+ subscriber = null
+ unsubscribe(this)
+ }
+
+ override fun request(p0: Long) {
+ (0 until p0).forEach { _ ->
+ if (initialControls.hasNext()) {
+ send(initialControls.next())
+ } else {
+ if (!indefinite) subscriber?.onComplete()
+ }
+ }
+ }
+
+ fun send(c: Control) {
+ Log.v(TAG, "sending update: " + Control_toString(c) + " => " + subscriber)
+ subscriber?.onNext(c)
+ }
+ }
+
+ override fun subscribe(subscriber: Flow.Subscriber<in Control>) {
+ Log.v(TAG, "subscribe to publisher: $this by subscriber: $subscriber")
+ val sub = UglySubscription(controlKeys.mapNotNull { controls[it] }.iterator(),
+ subscriber)
+ subscriptions.add(sub)
+ subscriber.onSubscribe(sub)
+ }
+
+ fun unsubscribe(sub: UglySubscription) {
+ Log.v(TAG, "no more subscriptions, removing subscriber: $sub")
+ subscriptions.remove(sub)
+ if (subscriptions.size == 0) {
+ Log.v(TAG, "no more subscribers, removing publisher: $this")
+ publishers.remove(this)
+ }
+ }
+
+ fun refresh() {
+ controlKeys.mapNotNull { controls[it] }.forEach { control ->
+ subscriptions.forEach { sub ->
+ sub.send(control)
+ }
+ }
+ }
+ }
+}
diff --git a/packages/EasterEgg/src/com/android/egg/neko/NekoDialog.java b/packages/EasterEgg/src/com/android/egg/neko/NekoDialog.java
new file mode 100644
index 000000000000..2bd2228e7bf2
--- /dev/null
+++ b/packages/EasterEgg/src/com/android/egg/neko/NekoDialog.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.egg.neko;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.egg.R;
+
+import java.util.ArrayList;
+
+public class NekoDialog extends Dialog {
+
+ private final Adapter mAdapter;
+
+ public NekoDialog(@NonNull Context context) {
+ super(context, android.R.style.Theme_Material_Dialog_NoActionBar);
+ RecyclerView view = new RecyclerView(getContext());
+ mAdapter = new Adapter(getContext());
+ view.setLayoutManager(new GridLayoutManager(getContext(), 2));
+ view.setAdapter(mAdapter);
+ final float dp = context.getResources().getDisplayMetrics().density;
+ final int pad = (int)(16*dp);
+ view.setPadding(pad, pad, pad, pad);
+ setContentView(view);
+ }
+
+ private void onFoodSelected(Food food) {
+ PrefState prefs = new PrefState(getContext());
+ int currentState = prefs.getFoodState();
+ if (currentState == 0 && food.getType() != 0) {
+ NekoService.registerJob(getContext(), food.getInterval(getContext()));
+ }
+// MetricsLogger.histogram(getContext(), "egg_neko_offered_food", food.getType());
+ prefs.setFoodState(food.getType());
+ dismiss();
+ }
+
+ private class Adapter extends RecyclerView.Adapter<Holder> {
+
+ private final Context mContext;
+ private final ArrayList<Food> mFoods = new ArrayList<>();
+
+ public Adapter(Context context) {
+ mContext = context;
+ int[] foods = context.getResources().getIntArray(R.array.food_names);
+ // skip food 0, you can't choose it
+ for (int i=1; i<foods.length; i++) {
+ mFoods.add(new Food(i));
+ }
+ }
+
+ @Override
+ public Holder onCreateViewHolder(ViewGroup parent, int viewType) {
+ return new Holder(LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.food_layout, parent, false));
+ }
+
+ @Override
+ public void onBindViewHolder(final Holder holder, int position) {
+ final Food food = mFoods.get(position);
+ ((ImageView) holder.itemView.findViewById(R.id.icon))
+ .setImageIcon(food.getIcon(mContext));
+ ((TextView) holder.itemView.findViewById(R.id.text))
+ .setText(food.getName(mContext));
+ holder.itemView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ onFoodSelected(mFoods.get(holder.getAdapterPosition()));
+ }
+ });
+ }
+
+ @Override
+ public int getItemCount() {
+ return mFoods.size();
+ }
+ }
+
+ public static class Holder extends RecyclerView.ViewHolder {
+
+ public Holder(View itemView) {
+ super(itemView);
+ }
+ }
+}
diff --git a/packages/EasterEgg/src/com/android/egg/neko/NekoLand.java b/packages/EasterEgg/src/com/android/egg/neko/NekoLand.java
new file mode 100644
index 000000000000..8ed808760dcd
--- /dev/null
+++ b/packages/EasterEgg/src/com/android/egg/neko/NekoLand.java
@@ -0,0 +1,340 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.egg.neko;
+
+import android.Manifest;
+import android.app.ActionBar;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.media.MediaScannerConnection;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.util.Log;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnLongClickListener;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.core.content.FileProvider;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.egg.R;
+import com.android.egg.neko.PrefState.PrefsListener;
+import com.android.internal.logging.MetricsLogger;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+public class NekoLand extends Activity implements PrefsListener {
+ public static String CHAN_ID = "EGG";
+
+ public static boolean DEBUG = false;
+ public static boolean DEBUG_NOTIFICATIONS = false;
+
+ private static final int EXPORT_BITMAP_SIZE = 600;
+
+ private static final int STORAGE_PERM_REQUEST = 123;
+
+ private static boolean CAT_GEN = false;
+ private PrefState mPrefs;
+ private CatAdapter mAdapter;
+ private Cat mPendingShareCat;
+
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.neko_activity);
+ final ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ actionBar.setLogo(Cat.create(this));
+ actionBar.setDisplayUseLogoEnabled(false);
+ actionBar.setDisplayShowHomeEnabled(true);
+ }
+
+ mPrefs = new PrefState(this);
+ mPrefs.setListener(this);
+ final RecyclerView recyclerView = findViewById(R.id.holder);
+ mAdapter = new CatAdapter();
+ recyclerView.setAdapter(mAdapter);
+ recyclerView.setLayoutManager(new GridLayoutManager(this, 3));
+ int numCats = updateCats();
+ MetricsLogger.histogram(this, "egg_neko_visit_gallery", numCats);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mPrefs.setListener(null);
+ }
+
+ private int updateCats() {
+ Cat[] cats;
+ if (CAT_GEN) {
+ cats = new Cat[50];
+ for (int i = 0; i < cats.length; i++) {
+ cats[i] = Cat.create(this);
+ }
+ } else {
+ final float[] hsv = new float[3];
+ List<Cat> list = mPrefs.getCats();
+ Collections.sort(list, new Comparator<Cat>() {
+ @Override
+ public int compare(Cat cat, Cat cat2) {
+ Color.colorToHSV(cat.getBodyColor(), hsv);
+ float bodyH1 = hsv[0];
+ Color.colorToHSV(cat2.getBodyColor(), hsv);
+ float bodyH2 = hsv[0];
+ return Float.compare(bodyH1, bodyH2);
+ }
+ });
+ cats = list.toArray(new Cat[0]);
+ }
+ mAdapter.setCats(cats);
+ return cats.length;
+ }
+
+ private void onCatClick(Cat cat) {
+ if (CAT_GEN) {
+ mPrefs.addCat(cat);
+ new AlertDialog.Builder(NekoLand.this)
+ .setTitle("Cat added")
+ .setPositiveButton(android.R.string.ok, null)
+ .show();
+ } else {
+ showNameDialog(cat);
+ }
+ }
+
+ private void onCatRemove(Cat cat) {
+ cat.logRemove(this);
+ mPrefs.removeCat(cat);
+ }
+
+ private void showNameDialog(final Cat cat) {
+ final Context context = new ContextThemeWrapper(this,
+ android.R.style.Theme_Material_Light_Dialog_NoActionBar);
+ // TODO: Move to XML, add correct margins.
+ View view = LayoutInflater.from(context).inflate(R.layout.edit_text, null);
+ final EditText text = (EditText) view.findViewById(android.R.id.edit);
+ text.setText(cat.getName());
+ text.setSelection(cat.getName().length());
+ final int size = context.getResources()
+ .getDimensionPixelSize(android.R.dimen.app_icon_size);
+ Drawable catIcon = cat.createIcon(this, size, size).loadDrawable(this);
+ new AlertDialog.Builder(context)
+ .setTitle(" ")
+ .setIcon(catIcon)
+ .setView(view)
+ .setPositiveButton(android.R.string.ok, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ cat.logRename(context);
+ cat.setName(text.getText().toString().trim());
+ mPrefs.addCat(cat);
+ }
+ }).show();
+ }
+
+ @Override
+ public void onPrefsChanged() {
+ updateCats();
+ }
+
+ private class CatAdapter extends RecyclerView.Adapter<CatHolder> {
+
+ private Cat[] mCats;
+
+ public void setCats(Cat[] cats) {
+ mCats = cats;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public CatHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ return new CatHolder(LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.cat_view, parent, false));
+ }
+
+ private void setContextGroupVisible(final CatHolder holder, boolean vis) {
+ final View group = holder.contextGroup;
+ if (vis && group.getVisibility() != View.VISIBLE) {
+ group.setAlpha(0);
+ group.setVisibility(View.VISIBLE);
+ group.animate().alpha(1.0f).setDuration(333);
+ Runnable hideAction = new Runnable() {
+ @Override
+ public void run() {
+ setContextGroupVisible(holder, false);
+ }
+ };
+ group.setTag(hideAction);
+ group.postDelayed(hideAction, 5000);
+ } else if (!vis && group.getVisibility() == View.VISIBLE) {
+ group.removeCallbacks((Runnable) group.getTag());
+ group.animate().alpha(0f).setDuration(250).withEndAction(new Runnable() {
+ @Override
+ public void run() {
+ group.setVisibility(View.INVISIBLE);
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(final CatHolder holder, int position) {
+ Context context = holder.itemView.getContext();
+ final int size = context.getResources().getDimensionPixelSize(R.dimen.neko_display_size);
+ holder.imageView.setImageIcon(mCats[position].createIcon(context, size, size));
+ holder.textView.setText(mCats[position].getName());
+ holder.itemView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ onCatClick(mCats[holder.getAdapterPosition()]);
+ }
+ });
+ holder.itemView.setOnLongClickListener(new OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View v) {
+ setContextGroupVisible(holder, true);
+ return true;
+ }
+ });
+ holder.delete.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ setContextGroupVisible(holder, false);
+ new AlertDialog.Builder(NekoLand.this)
+ .setTitle(getString(R.string.confirm_delete, mCats[position].getName()))
+ .setNegativeButton(android.R.string.cancel, null)
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ onCatRemove(mCats[holder.getAdapterPosition()]);
+ }
+ })
+ .show();
+ }
+ });
+ holder.share.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ setContextGroupVisible(holder, false);
+ Cat cat = mCats[holder.getAdapterPosition()];
+ if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ != PackageManager.PERMISSION_GRANTED) {
+ mPendingShareCat = cat;
+ requestPermissions(
+ new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
+ STORAGE_PERM_REQUEST);
+ return;
+ }
+ shareCat(cat);
+ }
+ });
+ }
+
+ @Override
+ public int getItemCount() {
+ return mCats.length;
+ }
+ }
+
+ private void shareCat(Cat cat) {
+ final File dir = new File(
+ Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
+ "Cats");
+ if (!dir.exists() && !dir.mkdirs()) {
+ Log.e("NekoLand", "save: error: can't create Pictures directory");
+ return;
+ }
+ final File png = new File(dir, cat.getName().replaceAll("[/ #:]+", "_") + ".png");
+ Bitmap bitmap = cat.createBitmap(EXPORT_BITMAP_SIZE, EXPORT_BITMAP_SIZE);
+ if (bitmap != null) {
+ try {
+ OutputStream os = new FileOutputStream(png);
+ bitmap.compress(Bitmap.CompressFormat.PNG, 0, os);
+ os.close();
+ MediaScannerConnection.scanFile(
+ this,
+ new String[]{png.toString()},
+ new String[]{"image/png"},
+ null);
+ Log.v("Neko", "cat file: " + png);
+ Uri uri = FileProvider.getUriForFile(this, "com.android.egg.fileprovider", png);
+ Log.v("Neko", "cat uri: " + uri);
+ Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.putExtra(Intent.EXTRA_STREAM, uri);
+ intent.putExtra(Intent.EXTRA_SUBJECT, cat.getName());
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ intent.setType("image/png");
+ startActivity(Intent.createChooser(intent, null)
+ .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION));
+ cat.logShare(this);
+ } catch (IOException e) {
+ Log.e("NekoLand", "save: error: " + e);
+ }
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode,
+ String permissions[], int[] grantResults) {
+ if (requestCode == STORAGE_PERM_REQUEST) {
+ if (mPendingShareCat != null) {
+ shareCat(mPendingShareCat);
+ mPendingShareCat = null;
+ }
+ }
+ }
+
+ private static class CatHolder extends RecyclerView.ViewHolder {
+ private final ImageView imageView;
+ private final TextView textView;
+ private final View contextGroup;
+ private final View delete;
+ private final View share;
+
+ public CatHolder(View itemView) {
+ super(itemView);
+ imageView = (ImageView) itemView.findViewById(android.R.id.icon);
+ textView = (TextView) itemView.findViewById(android.R.id.title);
+ contextGroup = itemView.findViewById(R.id.contextGroup);
+ delete = itemView.findViewById(android.R.id.closeButton);
+ share = itemView.findViewById(android.R.id.shareText);
+ }
+ }
+}
diff --git a/packages/EasterEgg/src/com/android/egg/neko/NekoLockedActivity.java b/packages/EasterEgg/src/com/android/egg/neko/NekoLockedActivity.java
new file mode 100644
index 000000000000..ca89adc7b6e4
--- /dev/null
+++ b/packages/EasterEgg/src/com/android/egg/neko/NekoLockedActivity.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.egg.neko;
+
+import android.app.Activity;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnDismissListener;
+import android.os.Bundle;
+import android.view.WindowManager;
+
+import androidx.annotation.Nullable;
+
+public class NekoLockedActivity extends Activity implements OnDismissListener {
+
+ private NekoDialog mDialog;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
+
+ mDialog = new NekoDialog(this);
+ mDialog.setOnDismissListener(this);
+ mDialog.show();
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ finish();
+ }
+}
diff --git a/packages/EasterEgg/src/com/android/egg/neko/NekoService.java b/packages/EasterEgg/src/com/android/egg/neko/NekoService.java
new file mode 100644
index 000000000000..939e85c07d06
--- /dev/null
+++ b/packages/EasterEgg/src/com/android/egg/neko/NekoService.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.egg.neko;
+
+import static com.android.egg.neko.Cat.PURR;
+import static com.android.egg.neko.NekoLand.CHAN_ID;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.ComponentName;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.android.egg.R;
+
+import java.util.List;
+import java.util.Random;
+
+public class NekoService extends JobService {
+
+ private static final String TAG = "NekoService";
+
+ public static int JOB_ID = 42;
+
+ public static int CAT_NOTIFICATION = 1;
+ public static int DEBUG_NOTIFICATION = 1234;
+
+ public static float CAT_CAPTURE_PROB = 1.0f; // generous
+
+ public static long SECONDS = 1000;
+ public static long MINUTES = 60 * SECONDS;
+
+ //public static long INTERVAL_FLEX = 15 * SECONDS;
+ public static long INTERVAL_FLEX = 5 * MINUTES;
+
+ public static float INTERVAL_JITTER_FRAC = 0.25f;
+
+ private static void setupNotificationChannels(Context context) {
+ NotificationManager noman = context.getSystemService(NotificationManager.class);
+ NotificationChannel eggChan = new NotificationChannel(CHAN_ID,
+ context.getString(R.string.notification_channel_name),
+ NotificationManager.IMPORTANCE_DEFAULT);
+ eggChan.setSound(Uri.EMPTY, Notification.AUDIO_ATTRIBUTES_DEFAULT); // cats are quiet
+ eggChan.setVibrationPattern(PURR); // not totally quiet though
+ //eggChan.setBlockableSystem(true); // unlike a real cat, you can push this one off your lap
+ eggChan.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC); // cats sit in the window
+ noman.createNotificationChannel(eggChan);
+ }
+
+ @Override
+ public boolean onStartJob(JobParameters params) {
+ Log.v(TAG, "Starting job: " + String.valueOf(params));
+
+ if (NekoLand.DEBUG_NOTIFICATIONS) {
+ NotificationManager noman = getSystemService(NotificationManager.class);
+ final Bundle extras = new Bundle();
+ extras.putString("android.substName", getString(R.string.notification_name));
+ final int size = getResources()
+ .getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
+ final Cat cat = Cat.create(this);
+ final Notification.Builder builder
+ = cat.buildNotification(this)
+ .setContentTitle("DEBUG")
+ .setChannelId(NekoLand.CHAN_ID)
+ .setContentText("Ran job: " + params);
+
+ noman.notify(DEBUG_NOTIFICATION, builder.build());
+ }
+
+ triggerFoodResponse(this);
+ cancelJob(this);
+ return false;
+ }
+
+ private static void triggerFoodResponse(Context context) {
+ final PrefState prefs = new PrefState(context);
+ int food = prefs.getFoodState();
+ if (food != 0) {
+ prefs.setFoodState(0); // nom
+ final Random rng = new Random();
+ if (rng.nextFloat() <= CAT_CAPTURE_PROB) {
+ Cat cat;
+ List<Cat> cats = prefs.getCats();
+ final int[] probs = context.getResources().getIntArray(R.array.food_new_cat_prob);
+ final float waterLevel100 = prefs.getWaterState() / 2; // water is 0..200
+ final float new_cat_prob = (float) ((food < probs.length)
+ ? probs[food]
+ : waterLevel100) / 100f;
+ Log.v(TAG, "Food type: " + food);
+ Log.v(TAG, "New cat probability: " + new_cat_prob);
+
+ if (cats.size() == 0 || rng.nextFloat() <= new_cat_prob) {
+ cat = newRandomCat(context, prefs);
+ Log.v(TAG, "A new cat is here: " + cat.getName());
+ } else {
+ cat = getExistingCat(prefs);
+ Log.v(TAG, "A cat has returned: " + cat.getName());
+ }
+
+ notifyCat(context, cat);
+ }
+ }
+ }
+
+ static void notifyCat(Context context, Cat cat) {
+ NotificationManager noman = context.getSystemService(NotificationManager.class);
+ final Notification.Builder builder = cat.buildNotification(context);
+ noman.notify(cat.getShortcutId(), CAT_NOTIFICATION, builder.build());
+ }
+
+ static Cat newRandomCat(Context context, PrefState prefs) {
+ final Cat cat = Cat.create(context);
+ prefs.addCat(cat);
+ cat.logAdd(context);
+ return cat;
+ }
+
+ static Cat getExistingCat(PrefState prefs) {
+ final List<Cat> cats = prefs.getCats();
+ if (cats.size() == 0) return null;
+ return cats.get(new Random().nextInt(cats.size()));
+ }
+
+ @Override
+ public boolean onStopJob(JobParameters jobParameters) {
+ return false;
+ }
+
+ public static void registerJobIfNeeded(Context context, long intervalMinutes) {
+ JobScheduler jss = context.getSystemService(JobScheduler.class);
+ JobInfo info = jss.getPendingJob(JOB_ID);
+ if (info == null) {
+ registerJob(context, intervalMinutes);
+ }
+ }
+
+ public static void registerJob(Context context, long intervalMinutes) {
+ setupNotificationChannels(context);
+
+ JobScheduler jss = context.getSystemService(JobScheduler.class);
+ jss.cancel(JOB_ID);
+ long interval = intervalMinutes * MINUTES;
+ long jitter = (long) (INTERVAL_JITTER_FRAC * interval);
+ interval += (long) (Math.random() * (2 * jitter)) - jitter;
+ final JobInfo jobInfo = new JobInfo.Builder(JOB_ID,
+ new ComponentName(context, NekoService.class))
+ .setPeriodic(interval, INTERVAL_FLEX)
+ .build();
+
+ Log.v(TAG, "A cat will visit in " + interval + "ms: " + String.valueOf(jobInfo));
+ jss.schedule(jobInfo);
+
+ if (NekoLand.DEBUG_NOTIFICATIONS) {
+ NotificationManager noman = context.getSystemService(NotificationManager.class);
+ noman.notify(DEBUG_NOTIFICATION, new Notification.Builder(context)
+ .setSmallIcon(R.drawable.stat_icon)
+ .setContentTitle(String.format("Job scheduled in %d min", (interval / MINUTES)))
+ .setContentText(String.valueOf(jobInfo))
+ .setPriority(Notification.PRIORITY_MIN)
+ .setCategory(Notification.CATEGORY_SERVICE)
+ .setChannelId(NekoLand.CHAN_ID)
+ .setShowWhen(true)
+ .build());
+ }
+ }
+
+ public static void cancelJob(Context context) {
+ JobScheduler jss = context.getSystemService(JobScheduler.class);
+ Log.v(TAG, "Canceling job");
+ jss.cancel(JOB_ID);
+ }
+}
diff --git a/packages/EasterEgg/src/com/android/egg/neko/NekoTile.java b/packages/EasterEgg/src/com/android/egg/neko/NekoTile.java
new file mode 100644
index 000000000000..d02433f40e89
--- /dev/null
+++ b/packages/EasterEgg/src/com/android/egg/neko/NekoTile.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.egg.neko;
+
+import android.content.Intent;
+import android.service.quicksettings.Tile;
+import android.service.quicksettings.TileService;
+import android.util.Log;
+
+import com.android.egg.neko.PrefState.PrefsListener;
+import com.android.internal.logging.MetricsLogger;
+
+public class NekoTile extends TileService implements PrefsListener {
+
+ private static final String TAG = "NekoTile";
+
+ private PrefState mPrefs;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mPrefs = new PrefState(this);
+ }
+
+ @Override
+ public void onStartListening() {
+ super.onStartListening();
+ mPrefs.setListener(this);
+ updateState();
+ }
+
+ @Override
+ public void onStopListening() {
+ super.onStopListening();
+ mPrefs.setListener(null);
+ }
+
+ @Override
+ public void onTileAdded() {
+ super.onTileAdded();
+ MetricsLogger.count(this, "egg_neko_tile_added", 1);
+ }
+
+ @Override
+ public void onTileRemoved() {
+ super.onTileRemoved();
+ MetricsLogger.count(this, "egg_neko_tile_removed", 1);
+ }
+
+ @Override
+ public void onPrefsChanged() {
+ updateState();
+ }
+
+ private void updateState() {
+ Tile tile = getQsTile();
+ int foodState = mPrefs.getFoodState();
+ Food food = new Food(foodState);
+ if (foodState != 0) {
+ NekoService.registerJobIfNeeded(this, food.getInterval(this));
+ }
+ tile.setIcon(food.getIcon(this));
+ tile.setLabel(food.getName(this));
+ tile.setState(foodState != 0 ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE);
+ tile.updateTile();
+ }
+
+ @Override
+ public void onClick() {
+ if (mPrefs.getFoodState() != 0) {
+ // there's already food loaded, let's empty it
+ MetricsLogger.count(this, "egg_neko_empty_food", 1);
+ mPrefs.setFoodState(0);
+ NekoService.cancelJob(this);
+ } else {
+ // time to feed the cats
+ if (isLocked()) {
+ if (isSecure()) {
+ Log.d(TAG, "startActivityAndCollapse");
+ Intent intent = new Intent(this, NekoLockedActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivityAndCollapse(intent);
+ } else {
+ unlockAndRun(new Runnable() {
+ @Override
+ public void run() {
+ showNekoDialog();
+ }
+ });
+ }
+ } else {
+ showNekoDialog();
+ }
+ }
+ }
+
+ private void showNekoDialog() {
+ Log.d(TAG, "showNekoDialog");
+ MetricsLogger.count(this, "egg_neko_select_food", 1);
+ showDialog(new NekoDialog(this));
+ }
+}
diff --git a/packages/EasterEgg/src/com/android/egg/neko/PrefState.java b/packages/EasterEgg/src/com/android/egg/neko/PrefState.java
new file mode 100644
index 000000000000..49ff315392b4
--- /dev/null
+++ b/packages/EasterEgg/src/com/android/egg/neko/PrefState.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.egg.neko;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class PrefState implements OnSharedPreferenceChangeListener {
+
+ private static final String FILE_NAME = "mPrefs";
+
+ private static final String FOOD_STATE = "food";
+
+ private static final String WATER_STATE = "water";
+
+ private static final String CAT_KEY_PREFIX = "cat:";
+
+ private final Context mContext;
+ private final SharedPreferences mPrefs;
+ private PrefsListener mListener;
+
+ public PrefState(Context context) {
+ mContext = context;
+ mPrefs = mContext.getSharedPreferences(FILE_NAME, 0);
+ }
+
+ // Can also be used for renaming.
+ public void addCat(Cat cat) {
+ mPrefs.edit()
+ .putString(CAT_KEY_PREFIX + String.valueOf(cat.getSeed()), cat.getName())
+ .apply();
+ }
+
+ public void removeCat(Cat cat) {
+ mPrefs.edit().remove(CAT_KEY_PREFIX + String.valueOf(cat.getSeed())).apply();
+ }
+
+ public List<Cat> getCats() {
+ ArrayList<Cat> cats = new ArrayList<>();
+ Map<String, ?> map = mPrefs.getAll();
+ for (String key : map.keySet()) {
+ if (key.startsWith(CAT_KEY_PREFIX)) {
+ long seed = Long.parseLong(key.substring(CAT_KEY_PREFIX.length()));
+ Cat cat = new Cat(mContext, seed);
+ cat.setName(String.valueOf(map.get(key)));
+ cats.add(cat);
+ }
+ }
+ return cats;
+ }
+
+ public int getFoodState() {
+ return mPrefs.getInt(FOOD_STATE, 0);
+ }
+
+ public void setFoodState(int foodState) {
+ mPrefs.edit().putInt(FOOD_STATE, foodState).apply();
+ }
+
+ public float getWaterState() {
+ return mPrefs.getFloat(WATER_STATE, 0f);
+ }
+
+ public void setWaterState(float waterState) {
+ mPrefs.edit().putFloat(WATER_STATE, waterState).apply();
+ }
+
+ public void setListener(PrefsListener listener) {
+ mListener = listener;
+ if (mListener != null) {
+ mPrefs.registerOnSharedPreferenceChangeListener(this);
+ } else {
+ mPrefs.unregisterOnSharedPreferenceChangeListener(this);
+ }
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ mListener.onPrefsChanged();
+ }
+
+ public interface PrefsListener {
+ void onPrefsChanged();
+ }
+}
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 5b6155180e0a..a7ef5e6f58f0 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -329,6 +329,7 @@
<activity android:name=".screenrecord.ScreenRecordDialog"
android:theme="@style/ScreenRecord"
+ android:showForAllUsers="true"
android:excludeFromRecents="true" />
<service android:name=".screenrecord.RecordingService" />
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index ccfbd8f57df1..b739999fb652 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -207,6 +207,12 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
/** Whether or not the BubbleStackView has been added to the WindowManager. */
private boolean mAddedToWindowManager = false;
+ /**
+ * Value from {@link NotificationShadeWindowController#getForceHasTopUi()} when we forced top UI
+ * due to expansion. We'll restore this value when the stack collapses.
+ */
+ private boolean mHadTopUi = false;
+
// Listens to user switch so bubbles can be saved and restored.
private final NotificationLockscreenUserManager mNotifUserManager;
@@ -1291,6 +1297,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
// Collapsing? Do this first before remaining steps.
if (update.expandedChanged && !update.expanded) {
mStackView.setExpanded(false);
+ mNotificationShadeWindowController.setForceHasTopUi(mHadTopUi);
}
// Do removals, if any.
@@ -1377,6 +1384,8 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
if (update.expandedChanged && update.expanded) {
if (mStackView != null) {
mStackView.setExpanded(true);
+ mHadTopUi = mNotificationShadeWindowController.getForceHasTopUi();
+ mNotificationShadeWindowController.setForceHasTopUi(true);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
index acbde9fa3efa..c170ee271e1d 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
@@ -368,6 +368,10 @@ public class BubbleData {
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
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
index b34312e2b473..2bfe015c2787 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
@@ -506,6 +506,14 @@ public class BubbleExpandedView extends LinearLayout {
}
}
+ @Nullable ActivityView getActivityView() {
+ return mActivityView;
+ }
+
+ int getTaskId() {
+ return mTaskId;
+ }
+
/**
* Called by {@link BubbleStackView} when the insets for the expanded state should be updated.
* This should be done post-move and post-animation.
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index 09e879926f26..1f3d981b8c9f 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -31,6 +31,7 @@ import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
+import android.app.ActivityView;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
@@ -293,11 +294,42 @@ public class BubbleStackView extends FrameLayout
/** Description of current animation controller state. */
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println("Stack view state:");
- pw.print(" gestureInProgress: "); pw.println(mIsGestureInProgress);
- pw.print(" showingDismiss: "); pw.println(mShowingDismiss);
- pw.print(" isExpansionAnimating: "); pw.println(mIsExpansionAnimating);
+ pw.print(" gestureInProgress: "); pw.println(mIsGestureInProgress);
+ pw.print(" showingDismiss: "); pw.println(mShowingDismiss);
+ pw.print(" isExpansionAnimating: "); pw.println(mIsExpansionAnimating);
+ pw.print(" expandedContainerVis: "); pw.println(mExpandedViewContainer.getVisibility());
+ pw.print(" expandedContainerAlpha: "); pw.println(mExpandedViewContainer.getAlpha());
+ pw.print(" expandedContainerMatrix: ");
+ pw.println(mExpandedViewContainer.getAnimationMatrix());
+
mStackAnimationController.dump(fd, pw, args);
mExpandedAnimationController.dump(fd, pw, args);
+
+ if (mExpandedBubble != null) {
+ pw.println("Expanded bubble state:");
+ pw.println(" expandedBubbleKey: " + mExpandedBubble.getKey());
+
+ final BubbleExpandedView expandedView = mExpandedBubble.getExpandedView();
+
+ if (expandedView != null) {
+ pw.println(" expandedViewVis: " + expandedView.getVisibility());
+ pw.println(" expandedViewAlpha: " + expandedView.getAlpha());
+ pw.println(" expandedViewTaskId: " + expandedView.getTaskId());
+
+ final ActivityView av = expandedView.getActivityView();
+
+ if (av != null) {
+ pw.println(" activityViewVis: " + av.getVisibility());
+ pw.println(" activityViewAlpha: " + av.getAlpha());
+ } else {
+ pw.println(" activityView is null");
+ }
+ } else {
+ pw.println("Expanded bubble view state: expanded bubble view is null");
+ }
+ } else {
+ pw.println("Expanded bubble state: expanded bubble is null");
+ }
}
private BubbleController.BubbleExpandListener mExpandListener;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
index 32ef063a55be..0c34b27d348e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
@@ -22,13 +22,13 @@ import android.text.TextUtils;
import android.util.Log;
import android.widget.Switch;
-import com.android.internal.logging.UiEventLogger;
import com.android.systemui.R;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.qs.QSTile;
import com.android.systemui.qs.QSHost;
import com.android.systemui.qs.tileimpl.QSTileImpl;
import com.android.systemui.screenrecord.RecordingController;
+import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
import javax.inject.Inject;
@@ -39,19 +39,17 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState>
implements RecordingController.RecordingStateChangeCallback {
private static final String TAG = "ScreenRecordTile";
private RecordingController mController;
- private ActivityStarter mActivityStarter;
+ private KeyguardDismissUtil mKeyguardDismissUtil;
private long mMillisUntilFinished = 0;
private Callback mCallback = new Callback();
- private UiEventLogger mUiEventLogger;
@Inject
public ScreenRecordTile(QSHost host, RecordingController controller,
- ActivityStarter activityStarter, UiEventLogger uiEventLogger) {
+ KeyguardDismissUtil keyguardDismissUtil) {
super(host);
mController = controller;
mController.observe(this, mCallback);
- mActivityStarter = activityStarter;
- mUiEventLogger = uiEventLogger;
+ mKeyguardDismissUtil = keyguardDismissUtil;
}
@Override
@@ -69,7 +67,7 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState>
} else if (mController.isRecording()) {
stopRecording();
} else {
- startCountdown();
+ mUiHandler.post(() -> showPrompt());
}
refreshState();
}
@@ -114,11 +112,15 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState>
return mContext.getString(R.string.quick_settings_screen_record_label);
}
- private void startCountdown() {
- // Close QS, otherwise the permission dialog appears beneath it
+ private void showPrompt() {
+ // Close QS, otherwise the dialog appears beneath it
getHost().collapsePanels();
Intent intent = mController.getPromptIntent();
- mActivityStarter.postStartActivityDismissingKeyguard(intent, 0);
+ ActivityStarter.OnDismissAction dismissAction = () -> {
+ mContext.startActivity(intent);
+ return false;
+ };
+ mKeyguardDismissUtil.executeWhenUnlocked(dismissAction, false);
}
private void cancelCountdown() {
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
index b253635e9bfa..82ac1f6f6a33 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
@@ -17,12 +17,17 @@
package com.android.systemui.screenrecord;
import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
import android.os.CountDownTimer;
+import android.os.UserHandle;
import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.statusbar.policy.CallbackController;
import java.util.ArrayList;
@@ -41,21 +46,30 @@ public class RecordingController
private static final String SYSUI_SCREENRECORD_LAUNCHER =
"com.android.systemui.screenrecord.ScreenRecordDialog";
- private final Context mContext;
private boolean mIsStarting;
private boolean mIsRecording;
private PendingIntent mStopIntent;
private CountDownTimer mCountDownTimer = null;
+ private BroadcastDispatcher mBroadcastDispatcher;
private ArrayList<RecordingStateChangeCallback> mListeners = new ArrayList<>();
+ @VisibleForTesting
+ protected final BroadcastReceiver mUserChangeReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (mStopIntent != null) {
+ stopRecording();
+ }
+ }
+ };
+
/**
* Create a new RecordingController
- * @param context Context for the controller
*/
@Inject
- public RecordingController(Context context) {
- mContext = context;
+ public RecordingController(BroadcastDispatcher broadcastDispatcher) {
+ mBroadcastDispatcher = broadcastDispatcher;
}
/**
@@ -99,6 +113,9 @@ public class RecordingController
}
try {
startIntent.send();
+ IntentFilter userFilter = new IntentFilter(Intent.ACTION_USER_SWITCHED);
+ mBroadcastDispatcher.registerReceiver(mUserChangeReceiver, userFilter, null,
+ UserHandle.ALL);
Log.d(TAG, "sent start intent");
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "Pending intent was cancelled: " + e.getMessage());
@@ -146,11 +163,16 @@ public class RecordingController
*/
public void stopRecording() {
try {
- mStopIntent.send();
+ if (mStopIntent != null) {
+ mStopIntent.send();
+ } else {
+ Log.e(TAG, "Stop intent was null");
+ }
updateState(false);
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "Error stopping: " + e.getMessage());
}
+ mBroadcastDispatcher.unregisterReceiver(mUserChangeReceiver);
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
index 87597263168a..476ec798a35f 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
@@ -32,6 +32,7 @@ import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
+import android.os.UserHandle;
import android.provider.Settings;
import android.util.Log;
import android.widget.Toast;
@@ -40,6 +41,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.LongRunning;
+import com.android.systemui.settings.CurrentUserContextTracker;
import java.io.IOException;
import java.util.concurrent.Executor;
@@ -58,7 +60,6 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
private static final String TAG = "RecordingService";
private static final String CHANNEL_ID = "screen_record";
private static final String EXTRA_RESULT_CODE = "extra_resultCode";
- private static final String EXTRA_DATA = "extra_data";
private static final String EXTRA_PATH = "extra_path";
private static final String EXTRA_AUDIO_SOURCE = "extra_useAudio";
private static final String EXTRA_SHOW_TAPS = "extra_showTaps";
@@ -79,14 +80,17 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
private final Executor mLongExecutor;
private final UiEventLogger mUiEventLogger;
private final NotificationManager mNotificationManager;
+ private final CurrentUserContextTracker mUserContextTracker;
@Inject
public RecordingService(RecordingController controller, @LongRunning Executor executor,
- UiEventLogger uiEventLogger, NotificationManager notificationManager) {
+ UiEventLogger uiEventLogger, NotificationManager notificationManager,
+ CurrentUserContextTracker userContextTracker) {
mController = controller;
mLongExecutor = executor;
mUiEventLogger = uiEventLogger;
mNotificationManager = notificationManager;
+ mUserContextTracker = userContextTracker;
}
/**
@@ -95,8 +99,6 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
* @param context Context from the requesting activity
* @param resultCode The result code from {@link android.app.Activity#onActivityResult(int, int,
* android.content.Intent)}
- * @param data The data from {@link android.app.Activity#onActivityResult(int, int,
- * android.content.Intent)}
* @param audioSource The ordinal value of the audio source
* {@link com.android.systemui.screenrecord.ScreenRecordingAudioSource}
* @param showTaps True to make touches visible while recording
@@ -118,6 +120,8 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
String action = intent.getAction();
Log.d(TAG, "onStartCommand " + action);
+ int mCurrentUserId = mUserContextTracker.getCurrentUserContext().getUserId();
+ UserHandle currentUser = new UserHandle(mCurrentUserId);
switch (action) {
case ACTION_START:
mAudioSource = ScreenRecordingAudioSource
@@ -132,8 +136,8 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
setTapsVisible(mShowTaps);
mRecorder = new ScreenMediaRecorder(
- getApplicationContext(),
- getUserId(),
+ mUserContextTracker.getCurrentUserContext(),
+ mCurrentUserId,
mAudioSource,
this
);
@@ -148,7 +152,14 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
} else {
mUiEventLogger.log(Events.ScreenRecordEvent.SCREEN_RECORD_END_QS_TILE);
}
- stopRecording();
+ // Check user ID - we may be getting a stop intent after user switch, in which case
+ // we want to post the notifications for that user, which is NOT current user
+ int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+ if (userId == -1) {
+ userId = mUserContextTracker.getCurrentUserContext().getUserId();
+ }
+ Log.d(TAG, "notifying for user " + userId);
+ stopRecording(userId);
mNotificationManager.cancel(NOTIFICATION_RECORDING_ID);
stopSelf();
break;
@@ -165,7 +176,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
// Remove notification
- mNotificationManager.cancel(NOTIFICATION_VIEW_ID);
+ mNotificationManager.cancelAsUser(null, NOTIFICATION_VIEW_ID, currentUser);
startActivity(Intent.createChooser(shareIntent, shareLabel)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
@@ -184,7 +195,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
Toast.LENGTH_LONG).show();
// Remove notification
- mNotificationManager.cancel(NOTIFICATION_VIEW_ID);
+ mNotificationManager.cancelAsUser(null, NOTIFICATION_VIEW_ID, currentUser);
Log.d(TAG, "Deleted recording " + uri);
break;
}
@@ -215,11 +226,12 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
mController.updateState(true);
createRecordingNotification();
mUiEventLogger.log(Events.ScreenRecordEvent.SCREEN_RECORD_START);
- } catch (IOException | RemoteException e) {
+ } catch (IOException | RemoteException | IllegalStateException e) {
Toast.makeText(this,
R.string.screenrecord_start_error, Toast.LENGTH_LONG)
.show();
e.printStackTrace();
+ mController.updateState(false);
}
}
@@ -242,7 +254,6 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
? res.getString(R.string.screenrecord_ongoing_screen_only)
: res.getString(R.string.screenrecord_ongoing_screen_and_audio);
-
Intent stopIntent = getNotificationIntent(this);
Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_screenrecord)
@@ -254,7 +265,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
.setOngoing(true)
.setContentIntent(
PendingIntent.getService(this, REQUEST_CODE, stopIntent,
- PendingIntent.FLAG_UPDATE_CURRENT))
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))
.addExtras(extras);
startForeground(NOTIFICATION_RECORDING_ID, builder.build());
}
@@ -265,11 +276,17 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
String notificationTitle = mAudioSource == ScreenRecordingAudioSource.NONE
? res.getString(R.string.screenrecord_ongoing_screen_only)
: res.getString(R.string.screenrecord_ongoing_screen_and_audio);
+
+ Bundle extras = new Bundle();
+ extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME,
+ res.getString(R.string.screenrecord_name));
+
Notification.Builder builder = new Notification.Builder(getApplicationContext(), CHANNEL_ID)
.setContentTitle(notificationTitle)
.setContentText(
getResources().getString(R.string.screenrecord_background_processing_label))
- .setSmallIcon(R.drawable.ic_screenrecord);
+ .setSmallIcon(R.drawable.ic_screenrecord)
+ .addExtras(extras);
return builder.build();
}
@@ -287,7 +304,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
this,
REQUEST_CODE,
getShareIntent(this, uri.toString()),
- PendingIntent.FLAG_UPDATE_CURRENT))
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))
.build();
Notification.Action deleteAction = new Notification.Action.Builder(
@@ -297,7 +314,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
this,
REQUEST_CODE,
getDeleteIntent(this, uri.toString()),
- PendingIntent.FLAG_UPDATE_CURRENT))
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))
.build();
Bundle extras = new Bundle();
@@ -328,34 +345,36 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
return builder.build();
}
- private void stopRecording() {
+ private void stopRecording(int userId) {
setTapsVisible(mOriginalShowTaps);
if (getRecorder() != null) {
getRecorder().end();
- saveRecording();
+ saveRecording(userId);
} else {
Log.e(TAG, "stopRecording called, but recorder was null");
}
mController.updateState(false);
}
- private void saveRecording() {
- mNotificationManager.notify(NOTIFICATION_PROCESSING_ID, createProcessingNotification());
+ private void saveRecording(int userId) {
+ UserHandle currentUser = new UserHandle(userId);
+ mNotificationManager.notifyAsUser(null, NOTIFICATION_PROCESSING_ID,
+ createProcessingNotification(), currentUser);
mLongExecutor.execute(() -> {
try {
Log.d(TAG, "saving recording");
Notification notification = createSaveNotification(getRecorder().save());
if (!mController.isRecording()) {
- Log.d(TAG, "showing saved notification");
- mNotificationManager.notify(NOTIFICATION_VIEW_ID, notification);
+ mNotificationManager.notifyAsUser(null, NOTIFICATION_VIEW_ID, notification,
+ currentUser);
}
} catch (IOException e) {
Log.e(TAG, "Error saving screen recording: " + e.getMessage());
Toast.makeText(this, R.string.screenrecord_delete_error, Toast.LENGTH_LONG)
.show();
} finally {
- mNotificationManager.cancel(NOTIFICATION_PROCESSING_ID);
+ mNotificationManager.cancelAsUser(null, NOTIFICATION_PROCESSING_ID, currentUser);
}
});
}
@@ -371,7 +390,9 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
* @return
*/
public static Intent getStopIntent(Context context) {
- return new Intent(context, RecordingService.class).setAction(ACTION_STOP);
+ return new Intent(context, RecordingService.class)
+ .setAction(ACTION_STOP)
+ .putExtra(Intent.EXTRA_USER_HANDLE, context.getUserId());
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenInternalAudioRecorder.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenInternalAudioRecorder.java
index edbc3cfdece5..df03c3e08f08 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenInternalAudioRecorder.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenInternalAudioRecorder.java
@@ -16,7 +16,6 @@
package com.android.systemui.screenrecord;
-import android.content.Context;
import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioPlaybackCaptureConfiguration;
@@ -39,7 +38,6 @@ public class ScreenInternalAudioRecorder {
private static String TAG = "ScreenAudioRecorder";
private static final int TIMEOUT = 500;
private static final float MIC_VOLUME_SCALE = 1.4f;
- private final Context mContext;
private AudioRecord mAudioRecord;
private AudioRecord mAudioRecordMic;
private Config mConfig = new Config();
@@ -49,17 +47,14 @@ public class ScreenInternalAudioRecorder {
private long mPresentationTime;
private long mTotalBytes;
private MediaMuxer mMuxer;
- private String mOutFile;
private boolean mMic;
private int mTrackId = -1;
- public ScreenInternalAudioRecorder(String outFile, Context context,
- MediaProjection mp, boolean includeMicInput) throws IOException {
+ public ScreenInternalAudioRecorder(String outFile, MediaProjection mp, boolean includeMicInput)
+ throws IOException {
mMic = includeMicInput;
- mOutFile = outFile;
mMuxer = new MediaMuxer(outFile, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
- mContext = context;
mMediaProjection = mp;
Log.d(TAG, "creating audio file " + outFile);
setupSimple();
@@ -266,8 +261,9 @@ public class ScreenInternalAudioRecorder {
/**
* start recording
+ * @throws IllegalStateException if recording fails to initialize
*/
- public void start() {
+ public void start() throws IllegalStateException {
if (mThread != null) {
Log.e(TAG, "a recording is being done in parallel or stop is not called");
}
@@ -276,8 +272,7 @@ public class ScreenInternalAudioRecorder {
Log.d(TAG, "channel count " + mAudioRecord.getChannelCount());
mCodec.start();
if (mAudioRecord.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) {
- Log.e(TAG, "Error starting audio recording");
- return;
+ throw new IllegalStateException("Audio recording failed to start");
}
mThread.start();
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java
index 1c7d987afff2..1a9abb9cf27d 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java
@@ -166,7 +166,7 @@ public class ScreenMediaRecorder {
mAudioSource == MIC_AND_INTERNAL) {
mTempAudioFile = File.createTempFile("temp", ".aac",
mContext.getCacheDir());
- mAudio = new ScreenInternalAudioRecorder(mTempAudioFile.getAbsolutePath(), mContext,
+ mAudio = new ScreenInternalAudioRecorder(mTempAudioFile.getAbsolutePath(),
mMediaProjection, mAudioSource == MIC_AND_INTERNAL);
}
@@ -175,7 +175,7 @@ public class ScreenMediaRecorder {
/**
* Start screen recording
*/
- void start() throws IOException, RemoteException {
+ void start() throws IOException, RemoteException, IllegalStateException {
Log.d(TAG, "start recording");
prepare();
mMediaRecorder.start();
@@ -205,7 +205,7 @@ public class ScreenMediaRecorder {
}
}
- private void recordInternalAudio() {
+ private void recordInternalAudio() throws IllegalStateException {
if (mAudioSource == INTERNAL || mAudioSource == MIC_AND_INTERNAL) {
mAudio.start();
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java
index 8347def2d430..dc47ab4dff63 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java
@@ -23,16 +23,19 @@ import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.NONE;
import android.app.Activity;
import android.app.PendingIntent;
+import android.content.Context;
import android.os.Bundle;
import android.view.Gravity;
import android.view.ViewGroup;
import android.view.Window;
+import android.view.WindowManager;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.Spinner;
import android.widget.Switch;
import com.android.systemui.R;
+import com.android.systemui.settings.CurrentUserContextTracker;
import java.util.ArrayList;
import java.util.List;
@@ -48,16 +51,17 @@ public class ScreenRecordDialog extends Activity {
private static final String TAG = "ScreenRecordDialog";
private final RecordingController mController;
+ private final CurrentUserContextTracker mCurrentUserContextTracker;
private Switch mTapsSwitch;
private Switch mAudioSwitch;
private Spinner mOptions;
private List<ScreenRecordingAudioSource> mModes;
- private int mSelected;
-
@Inject
- public ScreenRecordDialog(RecordingController controller) {
+ public ScreenRecordDialog(RecordingController controller,
+ CurrentUserContextTracker currentUserContextTracker) {
mController = controller;
+ mCurrentUserContextTracker = currentUserContextTracker;
}
@Override
@@ -68,6 +72,7 @@ public class ScreenRecordDialog extends Activity {
// Inflate the decor view, so the attributes below are not overwritten by the theme.
window.getDecorView();
window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ window.addPrivateFlags(WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS);
window.setGravity(Gravity.TOP);
setTitle(R.string.screenrecord_name);
@@ -100,24 +105,24 @@ public class ScreenRecordDialog extends Activity {
mOptions.setOnItemClickListenerInt((parent, view, position, id) -> {
mAudioSwitch.setChecked(true);
});
-
}
private void requestScreenCapture() {
+ Context userContext = mCurrentUserContextTracker.getCurrentUserContext();
boolean showTaps = mTapsSwitch.isChecked();
ScreenRecordingAudioSource audioMode = mAudioSwitch.isChecked()
? (ScreenRecordingAudioSource) mOptions.getSelectedItem()
: NONE;
- PendingIntent startIntent = PendingIntent.getForegroundService(this,
+ PendingIntent startIntent = PendingIntent.getForegroundService(userContext,
RecordingService.REQUEST_CODE,
RecordingService.getStartIntent(
- ScreenRecordDialog.this, RESULT_OK,
+ userContext, RESULT_OK,
audioMode.ordinal(), showTaps),
- PendingIntent.FLAG_UPDATE_CURRENT);
- PendingIntent stopIntent = PendingIntent.getService(this,
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+ PendingIntent stopIntent = PendingIntent.getService(userContext,
RecordingService.REQUEST_CODE,
- RecordingService.getStopIntent(this),
- PendingIntent.FLAG_UPDATE_CURRENT);
+ RecordingService.getStopIntent(userContext),
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
mController.startCountdown(DELAY_MS, INTERVAL_MS, startIntent, stopIntent);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
index 06d35a36e3c4..5bb8fab8a62e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
@@ -688,8 +688,8 @@ public class PhoneStatusBarPolicy
if (DEBUG) Log.d(TAG, "screenrecord: hiding icon during countdown");
mHandler.post(() -> mIconController.setIconVisibility(mSlotScreenRecord, false));
// Reset talkback priority
- mIconController.setIconAccessibilityLiveRegion(mSlotScreenRecord,
- View.ACCESSIBILITY_LIVE_REGION_NONE);
+ mHandler.post(() -> mIconController.setIconAccessibilityLiveRegion(mSlotScreenRecord,
+ View.ACCESSIBILITY_LIVE_REGION_NONE));
}
@Override
@@ -698,7 +698,7 @@ public class PhoneStatusBarPolicy
mIconController.setIcon(mSlotScreenRecord,
R.drawable.stat_sys_screen_record,
mResources.getString(R.string.screenrecord_ongoing_screen_only));
- mIconController.setIconVisibility(mSlotScreenRecord, true);
+ mHandler.post(() -> mIconController.setIconVisibility(mSlotScreenRecord, true));
}
@Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java
index e5024595d97e..5a6823879942 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java
@@ -29,13 +29,12 @@ import android.testing.TestableLooper;
import androidx.test.filters.SmallTest;
-import com.android.internal.logging.UiEventLogger;
import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
-import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.qs.QSTileHost;
import com.android.systemui.screenrecord.RecordingController;
+import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
import org.junit.Before;
import org.junit.Test;
@@ -44,18 +43,16 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
@SmallTest
public class ScreenRecordTileTest extends SysuiTestCase {
@Mock
private RecordingController mController;
@Mock
- private ActivityStarter mActivityStarter;
- @Mock
private QSTileHost mHost;
@Mock
- private UiEventLogger mUiEventLogger;
+ private KeyguardDismissUtil mKeyguardDismissUtil;
private TestableLooper mTestableLooper;
private ScreenRecordTile mTile;
@@ -67,11 +64,10 @@ public class ScreenRecordTileTest extends SysuiTestCase {
mTestableLooper = TestableLooper.get(this);
mDependency.injectTestDependency(Dependency.BG_LOOPER, mTestableLooper.getLooper());
mController = mDependency.injectMockDependency(RecordingController.class);
- mActivityStarter = mDependency.injectMockDependency(ActivityStarter.class);
when(mHost.getContext()).thenReturn(mContext);
- mTile = new ScreenRecordTile(mHost, mController, mActivityStarter, mUiEventLogger);
+ mTile = new ScreenRecordTile(mHost, mController, mKeyguardDismissUtil);
}
// Test that the tile is inactive and labeled correctly when the controller is neither starting
@@ -89,6 +85,7 @@ public class ScreenRecordTileTest extends SysuiTestCase {
mContext.getString(R.string.quick_settings_screen_record_start)));
mTile.handleClick();
+ mTestableLooper.processAllMessages();
verify(mController, times(1)).getPromptIntent();
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
index b877c7fa6859..11ef3e33f9d0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
@@ -22,12 +22,14 @@ import static junit.framework.Assert.assertTrue;
import static org.mockito.Mockito.verify;
import android.app.PendingIntent;
+import android.content.Intent;
import android.os.Looper;
import android.testing.AndroidTestingRunner;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.broadcast.BroadcastDispatcher;
import org.junit.Before;
import org.junit.Test;
@@ -45,14 +47,18 @@ import org.mockito.MockitoAnnotations;
public class RecordingControllerTest extends SysuiTestCase {
@Mock
- RecordingController.RecordingStateChangeCallback mCallback;
+ private RecordingController.RecordingStateChangeCallback mCallback;
+ @Mock
+ private BroadcastDispatcher mBroadcastDispatcher;
+
+ private RecordingController mController;
- RecordingController mController;
+ private static final int USER_ID = 10;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- mController = new RecordingController(mContext);
+ mController = new RecordingController(mBroadcastDispatcher);
mController.addCallback(mCallback);
}
@@ -121,4 +127,27 @@ public class RecordingControllerTest extends SysuiTestCase {
assertFalse(mController.isRecording());
verify(mCallback).onRecordingEnd();
}
+
+ // Test that switching users will stop an ongoing recording
+ @Test
+ public void testUserChange() {
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+
+ // If we are recording
+ PendingIntent startIntent = Mockito.mock(PendingIntent.class);
+ PendingIntent stopIntent = Mockito.mock(PendingIntent.class);
+ mController.startCountdown(0, 0, startIntent, stopIntent);
+ mController.updateState(true);
+
+ // and user is changed
+ Intent intent = new Intent(Intent.ACTION_USER_SWITCHED)
+ .putExtra(Intent.EXTRA_USER_HANDLE, USER_ID);
+ mController.mUserChangeReceiver.onReceive(mContext, intent);
+
+ // Ensure that the recording was stopped
+ verify(mCallback).onRecordingEnd();
+ assertFalse(mController.isRecording());
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
index 283a47ca3622..e98b6b69ee76 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
@@ -32,6 +32,7 @@ import androidx.test.filters.SmallTest;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.settings.CurrentUserContextTracker;
import org.junit.Before;
import org.junit.Test;
@@ -58,6 +59,8 @@ public class RecordingServiceTest extends SysuiTestCase {
private Notification mNotification;
@Mock
private Executor mExecutor;
+ @Mock
+ private CurrentUserContextTracker mUserContextTracker;
private RecordingService mRecordingService;
@@ -65,7 +68,7 @@ public class RecordingServiceTest extends SysuiTestCase {
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mRecordingService = Mockito.spy(new RecordingService(mController, mExecutor, mUiEventLogger,
- mNotificationManager));
+ mNotificationManager, mUserContextTracker));
// Return actual context info
doReturn(mContext).when(mRecordingService).getApplicationContext();
@@ -80,6 +83,8 @@ public class RecordingServiceTest extends SysuiTestCase {
doNothing().when(mRecordingService).startForeground(anyInt(), any());
doReturn(mScreenMediaRecorder).when(mRecordingService).getRecorder();
+
+ doReturn(mContext).when(mUserContextTracker).getCurrentUserContext();
}
@Test
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index be859069950e..e77b361c8c06 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -82,8 +82,6 @@ import static android.os.Process.removeAllProcessGroups;
import static android.os.Process.sendSignal;
import static android.os.Process.setThreadPriority;
import static android.os.Process.setThreadScheduler;
-import static android.permission.PermissionManager.KILL_APP_REASON_GIDS_CHANGED;
-import static android.permission.PermissionManager.KILL_APP_REASON_PERMISSIONS_REVOKED;
import static android.provider.Settings.Global.ALWAYS_FINISH_ACTIVITIES;
import static android.provider.Settings.Global.DEBUG_APP;
import static android.provider.Settings.Global.NETWORK_ACCESS_TIMEOUT_MS;
@@ -1669,12 +1667,6 @@ public class ActivityManagerService extends IActivityManager.Stub
*/
@Nullable ContentCaptureManagerInternal mContentCaptureService;
- /**
- * Set of {@link ProcessRecord} that have either {@link ProcessRecord#hasTopUi()} or
- * {@link ProcessRecord#runningRemoteAnimation} set to {@code true}.
- */
- final ArraySet<ProcessRecord> mTopUiOrRunningRemoteAnimApps = new ArraySet<>();
-
final class UiHandler extends Handler {
public UiHandler() {
super(com.android.server.UiThread.get().getLooper(), null, true);
@@ -9202,16 +9194,31 @@ public class ActivityManagerService extends IActivityManager.Stub
synchronized (this) {
final long identity = Binder.clearCallingIdentity();
try {
- boolean permissionChange = KILL_APP_REASON_PERMISSIONS_REVOKED.equals(reason)
- || KILL_APP_REASON_GIDS_CHANGED.equals(reason);
mProcessList.killPackageProcessesLocked(null /* packageName */, appId, userId,
ProcessList.PERSISTENT_PROC_ADJ, false /* callerWillRestart */,
true /* callerWillRestart */, true /* doit */, true /* evenPersistent */,
false /* setRemoved */,
- permissionChange ? ApplicationExitInfo.REASON_PERMISSION_CHANGE
- : ApplicationExitInfo.REASON_OTHER,
- permissionChange ? ApplicationExitInfo.SUBREASON_UNKNOWN
- : ApplicationExitInfo.SUBREASON_KILL_UID,
+ ApplicationExitInfo.REASON_OTHER,
+ ApplicationExitInfo.SUBREASON_KILL_UID,
+ reason != null ? reason : "kill uid");
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ }
+
+ @Override
+ public void killUidForPermissionChange(int appId, int userId, String reason) {
+ enforceCallingPermission(Manifest.permission.KILL_UID, "killUid");
+ synchronized (this) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ mProcessList.killPackageProcessesLocked(null /* packageName */, appId, userId,
+ ProcessList.PERSISTENT_PROC_ADJ, false /* callerWillRestart */,
+ true /* callerWillRestart */, true /* doit */, true /* evenPersistent */,
+ false /* setRemoved */,
+ ApplicationExitInfo.REASON_PERMISSION_CHANGE,
+ ApplicationExitInfo.SUBREASON_UNKNOWN,
reason != null ? reason : "kill uid");
} finally {
Binder.restoreCallingIdentity(identity);
@@ -14707,7 +14714,6 @@ public class ActivityManagerService extends IActivityManager.Stub
mProcessesToGc.remove(app);
mPendingPssProcesses.remove(app);
- mTopUiOrRunningRemoteAnimApps.remove(app);
ProcessList.abortNextPssTime(app.procStateMemTracker);
// Dismiss any open dialogs.
@@ -18508,22 +18514,6 @@ public class ActivityManagerService extends IActivityManager.Stub
return proc;
}
- /**
- * @return {@code true} if {@link #mTopUiOrRunningRemoteAnimApps} set contains {@code app} or when there are no apps
- * in this list, an false otherwise.
- */
- boolean containsTopUiOrRunningRemoteAnimOrEmptyLocked(ProcessRecord app) {
- return mTopUiOrRunningRemoteAnimApps.isEmpty() || mTopUiOrRunningRemoteAnimApps.contains(app);
- }
-
- void addTopUiOrRunningRemoteAnim(ProcessRecord app) {
- mTopUiOrRunningRemoteAnimApps.add(app);
- }
-
- void removeTopUiOrRunningRemoteAnim(ProcessRecord app) {
- mTopUiOrRunningRemoteAnimApps.remove(app);
- }
-
@Override
public boolean dumpHeap(String process, int userId, boolean managed, boolean mallocInfo,
boolean runGc, String path, ParcelFileDescriptor fd, RemoteCallback finishCallback) {
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index 58b0a157e2c2..da5f48962130 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -1151,17 +1151,8 @@ public final class OomAdjuster {
// is currently showing UI.
app.systemNoUi = true;
if (app == topApp) {
- // If specific system app has set ProcessRecord.mHasTopUi or is running a remote
- // animation (ProcessRecord.runningRemoteAnimation), this will prevent topApp
- // to use SCHED_GROUP_TOP_APP to ensure process with mHasTopUi will have exclusive
- // access to configured cores.
- if (mService.containsTopUiOrRunningRemoteAnimOrEmptyLocked(app)) {
- app.setCurrentSchedulingGroup(ProcessList.SCHED_GROUP_TOP_APP);
- } else {
- app.setCurrentSchedulingGroup(ProcessList.SCHED_GROUP_DEFAULT);
- }
app.systemNoUi = false;
-
+ app.setCurrentSchedulingGroup(ProcessList.SCHED_GROUP_TOP_APP);
app.adjType = "pers-top-activity";
} else if (app.hasTopUi()) {
// sched group/proc state adjustment is below
@@ -1202,20 +1193,10 @@ public final class OomAdjuster {
boolean foregroundActivities = false;
if (PROCESS_STATE_CUR_TOP == PROCESS_STATE_TOP && app == topApp) {
-
- // If specific system app has set ProcessRecord.mHasTopUi or is running a remote
- // animation (ProcessRecord.runningRemoteAnimation), this will prevent topApp
- // to use SCHED_GROUP_TOP_APP to ensure process with mHasTopUi will have exclusive
- // access to configured cores.
- if (mService.containsTopUiOrRunningRemoteAnimOrEmptyLocked(app)) {
- adj = ProcessList.FOREGROUND_APP_ADJ;
- schedGroup = ProcessList.SCHED_GROUP_TOP_APP;
- app.adjType = "top-activity";
- } else {
- adj = ProcessList.FOREGROUND_APP_ADJ;
- schedGroup = ProcessList.SCHED_GROUP_DEFAULT;
- app.adjType = "top-activity-behind-topui";
- }
+ // The last app on the list is the foreground app.
+ adj = ProcessList.FOREGROUND_APP_ADJ;
+ schedGroup = ProcessList.SCHED_GROUP_TOP_APP;
+ app.adjType = "top-activity";
foregroundActivities = true;
procState = PROCESS_STATE_CUR_TOP;
if (DEBUG_OOM_ADJ_REASON || logUid == appUid) {
diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java
index 4c75ab21d6f2..c5152c081e70 100644
--- a/services/core/java/com/android/server/am/ProcessRecord.java
+++ b/services/core/java/com/android/server/am/ProcessRecord.java
@@ -1268,7 +1268,6 @@ class ProcessRecord implements WindowProcessListener {
void setHasTopUi(boolean hasTopUi) {
mHasTopUi = hasTopUi;
mWindowProcessController.setHasTopUi(hasTopUi);
- updateTopUiOrRunningRemoteAnim();
}
boolean hasTopUi() {
@@ -1519,19 +1518,10 @@ class ProcessRecord implements WindowProcessListener {
Slog.i(TAG, "Setting runningRemoteAnimation=" + runningRemoteAnimation
+ " for pid=" + pid);
}
- updateTopUiOrRunningRemoteAnim();
mService.updateOomAdjLocked(this, true, OomAdjuster.OOM_ADJ_REASON_UI_VISIBILITY);
}
}
- void updateTopUiOrRunningRemoteAnim() {
- if (runningRemoteAnimation || hasTopUi()) {
- mService.addTopUiOrRunningRemoteAnim(this);
- } else {
- mService.removeTopUiOrRunningRemoteAnim(this);
- }
- }
-
public long getInputDispatchingTimeout() {
return mWindowProcessController.getInputDispatchingTimeout();
}
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index f8ab6f4a8b54..5e908b26fafa 100755
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -422,6 +422,9 @@ public class AudioService extends IAudioService.Stub
private final boolean mUseFixedVolume;
+ // If absolute volume is supported in AVRCP device
+ private volatile boolean mAvrcpAbsVolSupported = false;
+
/**
* Default stream type used for volume control in the absence of playback
* e.g. user on homescreen, no app playing anything, presses hardware volume buttons, this
@@ -4994,7 +4997,7 @@ public class AudioService extends IAudioService.Stub
return AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_MULTI_MODE;
}
if (audioSystemDeviceOut == AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP
- && mDeviceBroker.isAvrcpAbsoluteVolumeSupported()) {
+ && mAvrcpAbsVolSupported) {
return AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE;
}
return AudioManager.DEVICE_VOLUME_BEHAVIOR_VARIABLE;
@@ -5687,12 +5690,12 @@ public class AudioService extends IAudioService.Stub
}
// must be called while synchronized VolumeStreamState.class
- /*package*/ void applyDeviceVolume_syncVSS(int device, boolean isAvrcpAbsVolSupported) {
+ /*package*/ void applyDeviceVolume_syncVSS(int device) {
int index;
if (isFullyMuted()) {
index = 0;
} else if (AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device)
- && isAvrcpAbsVolSupported) {
+ && mAvrcpAbsVolSupported) {
index = getAbsoluteVolumeIndex((getIndex(device) + 5)/10);
} else if (isFullVolumeDevice(device)) {
index = (mIndexMax + 5)/10;
@@ -5705,7 +5708,6 @@ public class AudioService extends IAudioService.Stub
}
public void applyAllVolumes() {
- final boolean isAvrcpAbsVolSupported = mDeviceBroker.isAvrcpAbsoluteVolumeSupported();
synchronized (VolumeStreamState.class) {
// apply device specific volumes first
int index;
@@ -5715,7 +5717,7 @@ public class AudioService extends IAudioService.Stub
if (isFullyMuted()) {
index = 0;
} else if (AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device)
- && isAvrcpAbsVolSupported) {
+ && mAvrcpAbsVolSupported) {
index = getAbsoluteVolumeIndex((getIndex(device) + 5)/10);
} else if (isFullVolumeDevice(device)) {
index = (mIndexMax + 5)/10;
@@ -5955,7 +5957,6 @@ public class AudioService extends IAudioService.Stub
}
public void checkFixedVolumeDevices() {
- final boolean isAvrcpAbsVolSupported = mDeviceBroker.isAvrcpAbsoluteVolumeSupported();
synchronized (VolumeStreamState.class) {
// ignore settings for fixed volume devices: volume should always be at max or 0
if (mStreamVolumeAlias[mStreamType] == AudioSystem.STREAM_MUSIC) {
@@ -5966,7 +5967,7 @@ public class AudioService extends IAudioService.Stub
|| (isFixedVolumeDevice(device) && index != 0)) {
mIndexMap.put(device, mIndexMax);
}
- applyDeviceVolume_syncVSS(device, isAvrcpAbsVolSupported);
+ applyDeviceVolume_syncVSS(device);
}
}
}
@@ -6130,11 +6131,9 @@ public class AudioService extends IAudioService.Stub
/*package*/ void setDeviceVolume(VolumeStreamState streamState, int device) {
- final boolean isAvrcpAbsVolSupported = mDeviceBroker.isAvrcpAbsoluteVolumeSupported();
-
synchronized (VolumeStreamState.class) {
// Apply volume
- streamState.applyDeviceVolume_syncVSS(device, isAvrcpAbsVolSupported);
+ streamState.applyDeviceVolume_syncVSS(device);
// Apply change to all streams using this one as alias
int numStreamTypes = AudioSystem.getNumStreamTypes();
@@ -6144,13 +6143,11 @@ public class AudioService extends IAudioService.Stub
// Make sure volume is also maxed out on A2DP device for aliased stream
// that may have a different device selected
int streamDevice = getDeviceForStream(streamType);
- if ((device != streamDevice) && isAvrcpAbsVolSupported
+ if ((device != streamDevice) && mAvrcpAbsVolSupported
&& AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device)) {
- mStreamStates[streamType].applyDeviceVolume_syncVSS(device,
- isAvrcpAbsVolSupported);
+ mStreamStates[streamType].applyDeviceVolume_syncVSS(device);
}
- mStreamStates[streamType].applyDeviceVolume_syncVSS(streamDevice,
- isAvrcpAbsVolSupported);
+ mStreamStates[streamType].applyDeviceVolume_syncVSS(streamDevice);
}
}
}
@@ -6458,6 +6455,7 @@ public class AudioService extends IAudioService.Stub
// address is not used for now, but may be used when multiple a2dp devices are supported
sVolumeLogger.log(new AudioEventLogger.StringEvent("avrcpSupportsAbsoluteVolume addr="
+ address + " support=" + support));
+ mAvrcpAbsVolSupported = support;
mDeviceBroker.setAvrcpAbsoluteVolumeSupported(support);
sendMsg(mAudioHandler, MSG_SET_DEVICE_VOLUME, SENDMSG_QUEUE,
AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, 0,
@@ -7415,8 +7413,7 @@ public class AudioService extends IAudioService.Stub
pw.print(" mCameraSoundForced="); pw.println(mCameraSoundForced);
pw.print(" mHasVibrator="); pw.println(mHasVibrator);
pw.print(" mVolumePolicy="); pw.println(mVolumePolicy);
- pw.print(" mAvrcpAbsVolSupported=");
- pw.println(mDeviceBroker.isAvrcpAbsoluteVolumeSupported());
+ pw.print(" mAvrcpAbsVolSupported="); pw.println(mAvrcpAbsVolSupported);
pw.print(" mIsSingleVolume="); pw.println(mIsSingleVolume);
pw.print(" mUseFixedVolume="); pw.println(mUseFixedVolume);
pw.print(" mFixedVolumeDevices="); pw.println(dumpDeviceTypes(mFixedVolumeDevices));
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index 242132c8e5ff..b45d450cd200 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -59,6 +59,7 @@ import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
+import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Message;
import android.os.PowerManager;
@@ -119,6 +120,7 @@ public class MediaSessionService extends SystemService implements Monitor {
private final PowerManager.WakeLock mMediaEventWakeLock;
private final INotificationManager mNotificationManager;
private final Object mLock = new Object();
+ private final HandlerThread mRecordThread = new HandlerThread("SessionRecordThread");
// Keeps the full user id for each user.
@GuardedBy("mLock")
private final SparseIntArray mFullUserIds = new SparseIntArray();
@@ -198,6 +200,7 @@ public class MediaSessionService extends SystemService implements Monitor {
instantiateCustomProvider(null);
instantiateCustomDispatcher(null);
+ mRecordThread.start();
}
private boolean isGlobalPriorityActiveLocked() {
@@ -599,8 +602,8 @@ public class MediaSessionService extends SystemService implements Monitor {
final MediaSessionRecord session;
try {
session = new MediaSessionRecord(callerPid, callerUid, userId,
- callerPackageName, cb, tag, sessionInfo, this, mHandler.getLooper(),
- policies);
+ callerPackageName, cb, tag, sessionInfo, this,
+ mRecordThread.getLooper(), policies);
} catch (RemoteException e) {
throw new RuntimeException("Media Session owner died prematurely.", e);
}
@@ -1157,8 +1160,8 @@ public class MediaSessionService extends SystemService implements Monitor {
throw new SecurityException("Unexpected Session2Token's UID, expected=" + uid
+ " but actually=" + sessionToken.getUid());
}
- MediaSession2Record record = new MediaSession2Record(
- sessionToken, MediaSessionService.this, mHandler.getLooper(), 0);
+ MediaSession2Record record = new MediaSession2Record(sessionToken,
+ MediaSessionService.this, mRecordThread.getLooper(), 0);
synchronized (mLock) {
FullUserRecord user = getFullUserRecordLocked(record.getUserId());
user.mPriorityStack.addSession(record);
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index de8ad6b7db13..994cec2b1e59 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -401,6 +401,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
private boolean mDataLoaderFinished = false;
+ // TODO(b/159663586): should be protected by mLock
private IncrementalFileStorages mIncrementalFileStorages;
private static final FileFilter sAddedApkFilter = new FileFilter() {
@@ -1353,7 +1354,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
private boolean markAsSealed(@NonNull IntentSender statusReceiver, boolean forTransfer) {
Objects.requireNonNull(statusReceiver);
- List<PackageInstallerSession> childSessions = getChildSessions();
+ List<PackageInstallerSession> childSessions = getChildSessionsNotLocked();
synchronized (mLock) {
assertCallerIsOwnerOrRootLocked();
@@ -1436,7 +1437,11 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
*
* <p> This method is handy to prevent potential deadlocks (b/123391593)
*/
- private @Nullable List<PackageInstallerSession> getChildSessions() {
+ private @Nullable List<PackageInstallerSession> getChildSessionsNotLocked() {
+ if (Thread.holdsLock(mLock)) {
+ Slog.wtf(TAG, "Calling thread " + Thread.currentThread().getName()
+ + " is holding mLock", new Throwable());
+ }
List<PackageInstallerSession> childSessions = null;
if (isMultiPackage()) {
final int[] childSessionIds = getChildSessionIds();
@@ -1605,7 +1610,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
return;
}
}
- List<PackageInstallerSession> childSessions = getChildSessions();
+ List<PackageInstallerSession> childSessions = getChildSessionsNotLocked();
synchronized (mLock) {
try {
sealLocked(childSessions);
@@ -1649,7 +1654,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
throw new SecurityException("Can only transfer sessions that use public options");
}
- List<PackageInstallerSession> childSessions = getChildSessions();
+ List<PackageInstallerSession> childSessions = getChildSessionsNotLocked();
synchronized (mLock) {
assertCallerIsOwnerOrRootLocked();
@@ -1701,7 +1706,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
// outside of the lock, because reading the child
// sessions with the lock held could lead to deadlock
// (b/123391593).
- List<PackageInstallerSession> childSessions = getChildSessions();
+ List<PackageInstallerSession> childSessions = getChildSessionsNotLocked();
try {
synchronized (mLock) {
@@ -2602,6 +2607,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
"Session " + sessionId + " is a child of multi-package session "
+ mParentSessionId + " and may not be abandoned directly.");
}
+
+ List<PackageInstallerSession> childSessions = getChildSessionsNotLocked();
synchronized (mLock) {
if (params.isStaged && mDestroyed) {
// If a user abandons staged session in an unsafe state, then system will try to
@@ -2625,7 +2632,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
mCallback.onStagedSessionChanged(this);
return;
}
- cleanStageDir();
+ cleanStageDir(childSessions);
}
if (mRelinquished) {
@@ -3055,7 +3062,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
mStagedSessionErrorMessage = errorMessage;
Slog.d(TAG, "Marking session " + sessionId + " as failed: " + errorMessage);
}
- cleanStageDir();
+ cleanStageDirNotLocked();
mCallback.onStagedSessionChanged(this);
}
@@ -3070,7 +3077,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
mStagedSessionErrorMessage = "";
Slog.d(TAG, "Marking session " + sessionId + " as applied");
}
- cleanStageDir();
+ cleanStageDirNotLocked();
mCallback.onStagedSessionChanged(this);
}
@@ -3128,20 +3135,37 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
}
}
- private void cleanStageDir() {
- if (isMultiPackage()) {
- for (int childSessionId : getChildSessionIds()) {
- mSessionProvider.getSession(childSessionId).cleanStageDir();
+ /**
+ * <b>must not hold {@link #mLock}</b>
+ */
+ private void cleanStageDirNotLocked() {
+ if (Thread.holdsLock(mLock)) {
+ Slog.wtf(TAG, "Calling thread " + Thread.currentThread().getName()
+ + " is holding mLock", new Throwable());
+ }
+ cleanStageDir(getChildSessionsNotLocked());
+ }
+
+ private void cleanStageDir(List<PackageInstallerSession> childSessions) {
+ if (childSessions != null) {
+ for (PackageInstallerSession childSession : childSessions) {
+ if (childSession != null) {
+ childSession.cleanStageDir();
+ }
}
} else {
- if (mIncrementalFileStorages != null) {
- mIncrementalFileStorages.cleanUp();
- mIncrementalFileStorages = null;
- }
- try {
- mPm.mInstaller.rmPackageDir(stageDir.getAbsolutePath());
- } catch (InstallerException ignored) {
- }
+ cleanStageDir();
+ }
+ }
+
+ private void cleanStageDir() {
+ if (mIncrementalFileStorages != null) {
+ mIncrementalFileStorages.cleanUp();
+ mIncrementalFileStorages = null;
+ }
+ try {
+ mPm.mInstaller.rmPackageDir(stageDir.getAbsolutePath());
+ } catch (InstallerException ignored) {
}
}
diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
index 0dc4d131640c..1a7490e8b327 100644
--- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
+++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@@ -2281,7 +2281,7 @@ class PackageManagerShellCommand extends ShellCommand {
if (grant) {
mPermissionManager.grantRuntimePermission(pkg, perm, translatedUserId);
} else {
- mPermissionManager.revokeRuntimePermission(pkg, perm, translatedUserId);
+ mPermissionManager.revokeRuntimePermission(pkg, perm, translatedUserId, null);
}
return 0;
}
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
index 8879ccb97de3..4f0b689996f2 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
@@ -330,13 +330,17 @@ public class PermissionManagerService extends IPermissionManager.Stub {
mPackageManagerInt.writeSettings(true);
}
@Override
- public void onPermissionRevoked(int uid, int userId) {
+ public void onPermissionRevoked(int uid, int userId, String reason) {
mOnPermissionChangeListeners.onPermissionsChanged(uid);
// Critical; after this call the application should never have the permission
mPackageManagerInt.writeSettings(false);
final int appId = UserHandle.getAppId(uid);
- mHandler.post(() -> killUid(appId, userId, KILL_APP_REASON_PERMISSIONS_REVOKED));
+ if (reason == null) {
+ mHandler.post(() -> killUid(appId, userId, KILL_APP_REASON_PERMISSIONS_REVOKED));
+ } else {
+ mHandler.post(() -> killUid(appId, userId, reason));
+ }
}
@Override
public void onInstallPermissionRevoked() {
@@ -473,7 +477,7 @@ public class PermissionManagerService extends IPermissionManager.Stub {
IActivityManager am = ActivityManager.getService();
if (am != null) {
try {
- am.killUid(appId, userId, reason);
+ am.killUidForPermissionChange(appId, userId, reason);
} catch (RemoteException e) {
/* ignore - same process */
}
@@ -1529,19 +1533,21 @@ public class PermissionManagerService extends IPermissionManager.Stub {
}
@Override
- public void revokeRuntimePermission(String packageName, String permName, int userId) {
+ public void revokeRuntimePermission(String packageName, String permName, int userId,
+ String reason) {
final int callingUid = Binder.getCallingUid();
final boolean overridePolicy =
checkUidPermission(ADJUST_RUNTIME_PERMISSIONS_POLICY, callingUid)
== PackageManager.PERMISSION_GRANTED;
revokeRuntimePermissionInternal(permName, packageName, overridePolicy, callingUid, userId,
- mDefaultPermissionCallback);
+ reason, mDefaultPermissionCallback);
}
// TODO swap permission name and package name
private void revokeRuntimePermissionInternal(String permName, String packageName,
- boolean overridePolicy, int callingUid, final int userId, PermissionCallback callback) {
+ boolean overridePolicy, int callingUid, final int userId, String reason,
+ PermissionCallback callback) {
if (ApplicationPackageManager.DEBUG_TRACE_PERMISSION_UPDATES
&& ApplicationPackageManager.shouldTraceGrant(packageName, permName, userId)) {
Log.i(TAG, "System is revoking " + packageName + " "
@@ -1632,7 +1638,7 @@ public class PermissionManagerService extends IPermissionManager.Stub {
if (callback != null) {
callback.onPermissionRevoked(UserHandle.getUid(userId,
- UserHandle.getAppId(pkg.getUid())), userId);
+ UserHandle.getAppId(pkg.getUid())), userId, reason);
}
if (bp.isRuntime()) {
@@ -1706,7 +1712,7 @@ public class PermissionManagerService extends IPermissionManager.Stub {
mDefaultPermissionCallback.onInstallPermissionGranted();
}
- public void onPermissionRevoked(int uid, int userId) {
+ public void onPermissionRevoked(int uid, int userId, String reason) {
revokedPermissions.add(IntPair.of(uid, userId));
syncUpdatedUsers.add(userId);
@@ -1819,7 +1825,7 @@ public class PermissionManagerService extends IPermissionManager.Stub {
} else if ((flags & FLAG_PERMISSION_REVIEW_REQUIRED) == 0) {
// Otherwise, reset the permission.
revokeRuntimePermissionInternal(permName, packageName, false, Process.SYSTEM_UID,
- userId, delayingPermCallback);
+ userId, null, delayingPermCallback);
}
}
@@ -2300,7 +2306,7 @@ public class PermissionManagerService extends IPermissionManager.Stub {
try {
revokeRuntimePermissionInternal(permissionName, packageName,
- false, callingUid, userId, permissionCallback);
+ false, callingUid, userId, null, permissionCallback);
} catch (IllegalArgumentException e) {
Slog.e(TAG, "Could not revoke " + permissionName + " from "
+ packageName, e);
@@ -3886,7 +3892,7 @@ public class PermissionManagerService extends IPermissionManager.Stub {
PackageManagerServiceUtils.getPermissionsState(mPackageManagerInt,
pkg);
if (!newPermissionsState.hasPermission(permission, userId)) {
- callback.onPermissionRevoked(pkg.getUid(), userId);
+ callback.onPermissionRevoked(pkg.getUid(), userId, null);
break;
}
}
@@ -4245,7 +4251,7 @@ public class PermissionManagerService extends IPermissionManager.Stub {
overridePolicy,
Process.SYSTEM_UID,
userId,
- callback);
+ null, callback);
} catch (IllegalArgumentException e) {
Slog.e(TAG,
"Failed to revoke "
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java
index 4412162a5cc8..2e83b23f57d8 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java
@@ -156,7 +156,7 @@ public abstract class PermissionManagerServiceInternal extends PermissionManager
}
public void onInstallPermissionGranted() {
}
- public void onPermissionRevoked(int uid, @UserIdInt int userId) {
+ public void onPermissionRevoked(int uid, @UserIdInt int userId, String reason) {
}
public void onInstallPermissionRevoked() {
}
diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java
index b9240c78d711..9bc702d4c094 100644
--- a/services/core/java/com/android/server/wm/AccessibilityController.java
+++ b/services/core/java/com/android/server/wm/AccessibilityController.java
@@ -855,8 +855,7 @@ final class AccessibilityController {
mTempLayer = 0;
mDisplayContent.forAllWindows((w) -> {
if (w.isOnScreen() && w.isVisibleLw()
- && (w.mAttrs.alpha != 0)
- && !w.mWinAnimator.mEnterAnimationPending) {
+ && (w.mAttrs.alpha != 0)) {
mTempLayer++;
outWindows.put(mTempLayer, w);
}
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index afe40b8d8016..86ef0d82c1d1 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -3437,9 +3437,10 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
"Proposed new IME target: " + target + " for display: " + getDisplayId());
// Now, a special case -- if the last target's window is in the process of exiting, but
- // not removed, keep on the last target to avoid IME flicker.
+ // not removed, keep on the last target to avoid IME flicker. The exception is if the
+ // current target is home since we want opening apps to become the IME target right away.
if (curTarget != null && !curTarget.mRemoved && curTarget.isDisplayedLw()
- && curTarget.isClosing()) {
+ && curTarget.isClosing() && !curTarget.isActivityTypeHome()) {
if (DEBUG_INPUT_METHOD) Slog.v(TAG_WM, "Not changing target till current window is"
+ " closing and not removed");
return curTarget;
diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
index 1762b6229990..c8d9fe0f0a65 100644
--- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
@@ -351,13 +351,9 @@ class InsetsSourceProvider {
}
private void updateVisibility() {
- // TODO(b/159699383): remove the client controlled check when the insets visibility can be
- // driven by the system UI.
final boolean isClientControlled = mControlTarget != null
&& mControlTarget.isClientControlled();
- mSource.setVisible(mServerVisible
- && ((!isClientControlled && mDisplayContent.inMultiWindowMode())
- || mClientVisible));
+ mSource.setVisible(mServerVisible && (!isClientControlled || mClientVisible));
ProtoLog.d(WM_DEBUG_IME,
"InsetsSource updateVisibility serverVisible: %s clientVisible: %s",
mServerVisible, mClientVisible);
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index 7b690383f5f9..e23080724000 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -564,16 +564,23 @@ final class TaskDisplayArea extends DisplayArea<ActivityStack> {
// Apps and their containers are not allowed to specify an orientation while using
// root tasks...except for the home stack if it is not resizable and currently
// visible (top of) its root task.
- if (mRootHomeTask != null && mRootHomeTask.isVisible()
- && !mRootHomeTask.isResizeable()) {
+ if (mRootHomeTask != null && !mRootHomeTask.isResizeable()) {
// Manually nest one-level because because getOrientation() checks fillsParent()
// which checks that requestedOverrideBounds() is empty. However, in this case,
// it is not empty because it's been overridden to maintain the fullscreen size
// within a smaller split-root.
final Task topHomeTask = mRootHomeTask.getTopMostTask();
- final int orientation = topHomeTask.getOrientation();
- if (orientation != SCREEN_ORIENTATION_UNSET) {
- return orientation;
+ final ActivityRecord topHomeActivity = topHomeTask.getTopNonFinishingActivity();
+ // If a home activity is in the process of launching and isn't yet visible we
+ // should still respect the stack's preferred orientation to ensure rotation occurs
+ // before the home activity finishes launching.
+ final boolean isHomeActivityLaunching = topHomeActivity != null
+ && topHomeActivity.mVisibleRequested;
+ if (topHomeTask.isVisible() || isHomeActivityLaunching) {
+ final int orientation = topHomeTask.getOrientation();
+ if (orientation != SCREEN_ORIENTATION_UNSET) {
+ return orientation;
+ }
}
}
return SCREEN_ORIENTATION_UNSPECIFIED;
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
index cdafd32cbbb5..fde40aa77a0e 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
@@ -63,7 +63,6 @@ import static com.android.server.am.ProcessList.VISIBLE_APP_ADJ;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.AdditionalAnswers.answer;
-import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyLong;
@@ -171,7 +170,6 @@ public class MockingOomAdjusterTests {
mock(OomAdjProfiler.class));
doReturn(new ActivityManagerService.ProcessChangeItem()).when(sService)
.enqueueProcessChangeItemLocked(anyInt(), anyInt());
- doReturn(true).when(sService).containsTopUiOrRunningRemoteAnimOrEmptyLocked(any());
sService.mOomAdjuster = new OomAdjuster(sService, sService.mProcessList,
mock(ActiveUids.class));
sService.mOomAdjuster.mAdjSeq = 10000;
@@ -268,21 +266,6 @@ public class MockingOomAdjusterTests {
@SuppressWarnings("GuardedBy")
@Test
- public void testUpdateOomAdj_DoOne_TopApp_PreemptedByTopUi() {
- ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
- MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
- doReturn(PROCESS_STATE_TOP).when(sService.mAtmInternal).getTopProcessState();
- doReturn(app).when(sService).getTopAppLocked();
- doReturn(false).when(sService).containsTopUiOrRunningRemoteAnimOrEmptyLocked(eq(app));
- sService.mWakefulness = PowerManagerInternal.WAKEFULNESS_AWAKE;
- sService.mOomAdjuster.updateOomAdjLocked(app, false, OomAdjuster.OOM_ADJ_REASON_NONE);
- doReturn(null).when(sService).getTopAppLocked();
-
- assertProcStates(app, PROCESS_STATE_TOP, FOREGROUND_APP_ADJ, SCHED_GROUP_DEFAULT);
- }
-
- @SuppressWarnings("GuardedBy")
- @Test
public void testUpdateOomAdj_DoOne_RunningInstrumentation() {
ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
diff --git a/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java
index 37e29b7bf18d..924ad7f3f5a1 100644
--- a/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java
@@ -174,10 +174,10 @@ public class BlobStoreManagerServiceTest {
mService.handlePackageRemoved(TEST_PKG1, TEST_UID1);
// Verify sessions are removed
- verify(sessionFile1).delete();
- verify(sessionFile2, never()).delete();
- verify(sessionFile3, never()).delete();
- verify(sessionFile4).delete();
+ verify(session1).destroy();
+ verify(session2, never()).destroy();
+ verify(session3, never()).destroy();
+ verify(session4).destroy();
assertThat(mUserSessions.size()).isEqualTo(2);
assertThat(mUserSessions.get(sessionId1)).isNull();
@@ -193,9 +193,9 @@ public class BlobStoreManagerServiceTest {
verify(blobMetadata3).removeCommitter(TEST_PKG1, TEST_UID1);
verify(blobMetadata3).removeLeasee(TEST_PKG1, TEST_UID1);
- verify(blobFile1, never()).delete();
- verify(blobFile2).delete();
- verify(blobFile3).delete();
+ verify(blobMetadata1, never()).destroy();
+ verify(blobMetadata2).destroy();
+ verify(blobMetadata3).destroy();
assertThat(mUserBlobs.size()).isEqualTo(1);
assertThat(mUserBlobs.get(blobHandle1)).isNotNull();
@@ -272,9 +272,9 @@ public class BlobStoreManagerServiceTest {
mService.handleIdleMaintenanceLocked();
// Verify stale sessions are removed
- verify(sessionFile1).delete();
- verify(sessionFile2, never()).delete();
- verify(sessionFile3).delete();
+ verify(session1).destroy();
+ verify(session2, never()).destroy();
+ verify(session3).destroy();
assertThat(mUserSessions.size()).isEqualTo(1);
assertThat(mUserSessions.get(sessionId2)).isNotNull();
@@ -317,9 +317,9 @@ public class BlobStoreManagerServiceTest {
mService.handleIdleMaintenanceLocked();
// Verify stale blobs are removed
- verify(blobFile1).delete();
- verify(blobFile2, never()).delete();
- verify(blobFile3).delete();
+ verify(blobMetadata1).destroy();
+ verify(blobMetadata2, never()).destroy();
+ verify(blobMetadata3).destroy();
assertThat(mUserBlobs.size()).isEqualTo(1);
assertThat(mUserBlobs.get(blobHandle2)).isNotNull();
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index ef4d5db2f32f..16aa87b3e59c 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -1339,14 +1339,15 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
- @Ignore
public void testPostCancelPostNotifiesListeners() throws Exception {
// WHEN a notification is posted
final StatusBarNotification sbn = generateNotificationRecord(null).getSbn();
mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", sbn.getId(),
sbn.getNotification(), sbn.getUserId());
+ Thread.sleep(1); // make sure the system clock advances before the next step
// THEN it is canceled
mBinderService.cancelNotificationWithTag(PKG, PKG, "tag", sbn.getId(), sbn.getUserId());
+ Thread.sleep(1); // here too
// THEN it is posted again (before the cancel has a chance to finish)
mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", sbn.getId(),
sbn.getNotification(), sbn.getUserId());
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
index 786f8d8af024..8c3661b409f4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
@@ -28,14 +28,17 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.server.wm.ActivityStack.ActivityState.RESUMED;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -207,6 +210,40 @@ public class TaskDisplayAreaTests extends WindowTestsBase {
false /* reuseCandidate */);
}
+ @Test
+ public void testGetOrientation_nonResizableHomeStackWithHomeActivityPendingVisibilityChange() {
+ final RootWindowContainer rootWindowContainer = mWm.mAtmService.mRootWindowContainer;
+ final TaskDisplayArea defaultTaskDisplayArea =
+ rootWindowContainer.getDefaultTaskDisplayArea();
+
+ final ActivityStack rootHomeTask = defaultTaskDisplayArea.getRootHomeTask();
+ rootHomeTask.mResizeMode = RESIZE_MODE_UNRESIZEABLE;
+
+ final ActivityStack primarySplitTask =
+ new ActivityTestsBase.StackBuilder(rootWindowContainer)
+ .setTaskDisplayArea(defaultTaskDisplayArea)
+ .setWindowingMode(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY)
+ .setActivityType(ACTIVITY_TYPE_STANDARD)
+ .setOnTop(true)
+ .setCreateActivity(true)
+ .build();
+ ActivityRecord primarySplitActivity = primarySplitTask.getTopNonFinishingActivity();
+ assertNotNull(primarySplitActivity);
+ primarySplitActivity.setState(RESUMED,
+ "testGetOrientation_nonResizableHomeStackWithHomeActivityPendingVisibilityChange");
+
+ ActivityRecord homeActivity = rootHomeTask.getTopNonFinishingActivity();
+ if (homeActivity == null) {
+ homeActivity = new ActivityTestsBase.ActivityBuilder(mWm.mAtmService)
+ .setStack(rootHomeTask).setCreateTask(true).build();
+ }
+ homeActivity.setVisible(false);
+ homeActivity.mVisibleRequested = true;
+ assertFalse(rootHomeTask.isVisible());
+
+ assertEquals(rootWindowContainer.getOrientation(), rootHomeTask.getOrientation());
+ }
+
private void assertGetOrCreateStack(int windowingMode, int activityType, Task candidateTask,
boolean reuseCandidate) {
final TaskDisplayArea taskDisplayArea = candidateTask.getDisplayArea();