diff options
148 files changed, 4617 insertions, 449 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 3d4154a227be..0b760a621d22 100644 --- a/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java +++ b/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java @@ -154,10 +154,10 @@ class BlobMetadata { } } - void removeInvalidCommitters(SparseArray<String> packages) { + void removeCommittersFromUnknownPkgs(SparseArray<String> knownPackages) { synchronized (mMetadataLock) { mCommitters.removeIf(committer -> - !committer.packageName.equals(packages.get(committer.uid))); + !committer.packageName.equals(knownPackages.get(committer.uid))); } } @@ -200,16 +200,27 @@ class BlobMetadata { } } - void removeInvalidLeasees(SparseArray<String> packages) { + void removeLeaseesFromUnknownPkgs(SparseArray<String> knownPackages) { synchronized (mMetadataLock) { mLeasees.removeIf(leasee -> - !leasee.packageName.equals(packages.get(leasee.uid))); + !leasee.packageName.equals(knownPackages.get(leasee.uid))); } } - boolean hasLeases() { + void removeExpiredLeases() { synchronized (mMetadataLock) { - return !mLeasees.isEmpty(); + mLeasees.removeIf(leasee -> !leasee.isStillValid()); + } + } + + boolean hasValidLeases() { + synchronized (mMetadataLock) { + for (int i = 0, size = mLeasees.size(); i < size; ++i) { + if (mLeasees.valueAt(i).isStillValid()) { + return true; + } + } + return false; } } @@ -226,8 +237,7 @@ class BlobMetadata { // Check if packageName already holds a lease on the blob. for (int i = 0, size = mLeasees.size(); i < size; ++i) { final Leasee leasee = mLeasees.valueAt(i); - if (leasee.equals(callingPackage, callingUid) - && leasee.isStillValid()) { + if (leasee.isStillValid() && leasee.equals(callingPackage, callingUid)) { return true; } } @@ -259,25 +269,32 @@ class BlobMetadata { boolean isALeasee(@Nullable String packageName, int uid) { synchronized (mMetadataLock) { - return isAnAccessor(mLeasees, packageName, uid); + final Leasee leasee = getAccessor(mLeasees, packageName, uid); + return leasee != null && leasee.isStillValid(); } } private static <T extends Accessor> boolean isAnAccessor(@NonNull ArraySet<T> accessors, @Nullable String packageName, int uid) { // Check if the package is an accessor of the data blob. + return getAccessor(accessors, packageName, uid) != null; + } + + private static <T extends Accessor> T getAccessor(@NonNull ArraySet<T> accessors, + @Nullable String packageName, int uid) { + // Check if the package is an accessor of the data blob. for (int i = 0, size = accessors.size(); i < size; ++i) { final Accessor accessor = accessors.valueAt(i); if (packageName != null && uid != INVALID_UID && accessor.equals(packageName, uid)) { - return true; + return (T) accessor; } else if (packageName != null && accessor.packageName.equals(packageName)) { - return true; + return (T) accessor; } else if (uid != INVALID_UID && accessor.uid == uid) { - return true; + return (T) accessor; } } - return false; + return null; } boolean isALeasee(@NonNull String packageName) { @@ -298,11 +315,11 @@ class BlobMetadata { private boolean hasOtherLeasees(@Nullable String packageName, int uid) { synchronized (mMetadataLock) { - if (mCommitters.size() > 1 || mLeasees.size() > 1) { - return true; - } for (int i = 0, size = mLeasees.size(); i < size; ++i) { final Leasee leasee = mLeasees.valueAt(i); + if (!leasee.isStillValid()) { + continue; + } // TODO: Also exclude packages which are signed with same cert? if (packageName != null && uid != INVALID_UID && !leasee.equals(packageName, uid)) { @@ -322,6 +339,9 @@ class BlobMetadata { synchronized (mMetadataLock) { for (int i = 0, size = mLeasees.size(); i < size; ++i) { final Leasee leasee = mLeasees.valueAt(i); + if (!leasee.isStillValid()) { + continue; + } if (leasee.uid == uid && leasee.packageName.equals(packageName)) { final int descriptionResId = leasee.descriptionResEntryName == null ? Resources.ID_NULL @@ -426,7 +446,7 @@ class BlobMetadata { // Blobs with no active leases if ((!respectLeaseWaitTime || hasLeaseWaitTimeElapsedForAll()) - && !hasLeases()) { + && !hasValidLeases()) { return true; } @@ -715,7 +735,7 @@ class BlobMetadata { } boolean isStillValid() { - return expiryTimeMillis == 0 || expiryTimeMillis <= System.currentTimeMillis(); + return expiryTimeMillis == 0 || expiryTimeMillis >= System.currentTimeMillis(); } void dump(@NonNull Context context, @NonNull IndentingPrintWriter fout) { diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java index f7468d8faa62..d9c0e478f1e3 100644 --- a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java +++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java @@ -539,7 +539,7 @@ public class BlobStoreManagerService extends SystemService { Slog.v(TAG, "Released lease on " + blobHandle + "; callingUid=" + callingUid + ", callingPackage=" + callingPackage); } - if (!blobMetadata.hasLeases()) { + if (!blobMetadata.hasValidLeases()) { mHandler.postDelayed(() -> { synchronized (mBlobsLock) { // Check if blobMetadata object is still valid. If it is not, then @@ -583,6 +583,9 @@ public class BlobStoreManagerService extends SystemService { getUserBlobsLocked(userId).forEach((blobHandle, blobMetadata) -> { final ArrayList<LeaseInfo> leaseInfos = new ArrayList<>(); blobMetadata.forEachLeasee(leasee -> { + if (!leasee.isStillValid()) { + return; + } final int descriptionResId = leasee.descriptionResEntryName == null ? Resources.ID_NULL : getDescriptionResourceId(resourcesGetter.apply(leasee.packageName), @@ -922,8 +925,8 @@ public class BlobStoreManagerService extends SystemService { blobMetadata.getBlobFile().delete(); } else { addBlobForUserLocked(blobMetadata, blobMetadata.getUserId()); - blobMetadata.removeInvalidCommitters(userPackages); - blobMetadata.removeInvalidLeasees(userPackages); + blobMetadata.removeCommittersFromUnknownPkgs(userPackages); + blobMetadata.removeLeaseesFromUnknownPkgs(userPackages); } mCurrentMaxSessionId = Math.max(mCurrentMaxSessionId, blobMetadata.getBlobId()); } @@ -1110,6 +1113,9 @@ public class BlobStoreManagerService extends SystemService { userBlobs.entrySet().removeIf(entry -> { final BlobMetadata blobMetadata = entry.getValue(); + // Remove expired leases + blobMetadata.removeExpiredLeases(); + if (blobMetadata.shouldBeDeleted(true /* respectLeaseWaitTime */)) { deleteBlobLocked(blobMetadata); deletedBlobIds.add(blobMetadata.getBlobId()); diff --git a/apex/extservices/apex_manifest.json b/apex/extservices/apex_manifest.json index 47b731585174..56531ebca8d4 100644 --- a/apex/extservices/apex_manifest.json +++ b/apex/extservices/apex_manifest.json @@ -1,4 +1,4 @@ { "name": "com.android.extservices", - "version": 300802700 + "version": 300802800 } diff --git a/apex/permission/apex_manifest.json b/apex/permission/apex_manifest.json index c452316cb6b0..94591d6a69e1 100644 --- a/apex/permission/apex_manifest.json +++ b/apex/permission/apex_manifest.json @@ -1,4 +1,4 @@ { "name": "com.android.permission", - "version": 300802700 + "version": 300802800 } diff --git a/apex/statsd/apex_manifest.json b/apex/statsd/apex_manifest.json index d56fe3c3467f..aa87b25d65fc 100644 --- a/apex/statsd/apex_manifest.json +++ b/apex/statsd/apex_manifest.json @@ -1,5 +1,5 @@ { "name": "com.android.os.statsd", - "version": 300802700 + "version": 300802800 } 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/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/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/java/com/android/internal/app/SuspendedAppActivity.java b/core/java/com/android/internal/app/SuspendedAppActivity.java index 0589baa76b8a..d8eaeda2b549 100644 --- a/core/java/com/android/internal/app/SuspendedAppActivity.java +++ b/core/java/com/android/internal/app/SuspendedAppActivity.java @@ -26,6 +26,7 @@ import android.Manifest; import android.annotation.Nullable; import android.app.AlertDialog; import android.app.AppGlobals; +import android.app.KeyguardManager; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentSender; @@ -208,9 +209,32 @@ public class SuspendedAppActivity extends AlertActivity ap.mPositiveButtonText = getString(android.R.string.ok); ap.mNeutralButtonText = resolveNeutralButtonText(); ap.mPositiveButtonListener = ap.mNeutralButtonListener = this; + + requestDismissKeyguardIfNeeded(ap.mMessage); + setupAlert(); } + private void requestDismissKeyguardIfNeeded(CharSequence dismissMessage) { + final KeyguardManager km = getSystemService(KeyguardManager.class); + if (km.isKeyguardLocked()) { + km.requestDismissKeyguard(this, dismissMessage, + new KeyguardManager.KeyguardDismissCallback() { + @Override + public void onDismissError() { + Slog.e(TAG, "Error while dismissing keyguard." + + " Keeping the dialog visible."); + } + + @Override + public void onDismissCancelled() { + Slog.w(TAG, "Keyguard dismiss was cancelled. Finishing."); + SuspendedAppActivity.this.finish(); + } + }); + } + } + @Override public void onClick(DialogInterface dialog, int which) { switch (which) { diff --git a/core/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/tests/bugreports/src/android/server/bugreports/BugreportManagerTest.java b/core/tests/bugreports/src/android/server/bugreports/BugreportManagerTest.java index c72707db9560..153337727e96 100644 --- a/core/tests/bugreports/src/android/server/bugreports/BugreportManagerTest.java +++ b/core/tests/bugreports/src/android/server/bugreports/BugreportManagerTest.java @@ -58,6 +58,8 @@ public class BugreportManagerTest { private Handler mHandler; private Executor mExecutor; private BugreportManager mBrm; + private File mBugreportFile; + private File mScreenshotFile; private ParcelFileDescriptor mBugreportFd; private ParcelFileDescriptor mScreenshotFd; @@ -73,8 +75,10 @@ public class BugreportManagerTest { }; mBrm = getBugreportManager(); - mBugreportFd = parcelFd("bugreport_" + name.getMethodName(), ".zip"); - mScreenshotFd = parcelFd("screenshot_" + name.getMethodName(), ".png"); + mBugreportFile = createTempFile("bugreport_" + name.getMethodName(), ".zip"); + mScreenshotFile = createTempFile("screenshot_" + name.getMethodName(), ".png"); + mBugreportFd = parcelFd(mBugreportFile); + mScreenshotFd = parcelFd(mScreenshotFile); getPermissions(); } @@ -121,6 +125,21 @@ public class BugreportManagerTest { } @Test + public void normalFlow_full() throws Exception { + BugreportCallbackImpl callback = new BugreportCallbackImpl(); + mBrm.startBugreport(mBugreportFd, mScreenshotFd, full(), mExecutor, callback); + + waitTillDoneOrTimeout(callback); + assertThat(callback.isDone()).isTrue(); + assertThat(callback.getErrorCode()).isEqualTo( + BugreportCallback.BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT); + // bugreport and screenshot files should be empty when user consent timed out. + assertThat(mBugreportFile.length()).isEqualTo(0); + assertThat(mScreenshotFile.length()).isEqualTo(0); + assertFdsAreClosed(mBugreportFd, mScreenshotFd); + } + + @Test public void simultaneousBugreportsNotAllowed() throws Exception { // Start bugreport #1 BugreportCallbackImpl callback = new BugreportCallbackImpl(); @@ -129,9 +148,10 @@ public class BugreportManagerTest { // Before #1 is done, try to start #2. assertThat(callback.isDone()).isFalse(); BugreportCallbackImpl callback2 = new BugreportCallbackImpl(); - ParcelFileDescriptor bugreportFd2 = parcelFd("bugreport_2_" + name.getMethodName(), ".zip"); - ParcelFileDescriptor screenshotFd2 = - parcelFd("screenshot_2_" + name.getMethodName(), ".png"); + File bugreportFile2 = createTempFile("bugreport_2_" + name.getMethodName(), ".zip"); + File screenshotFile2 = createTempFile("screenshot_2_" + name.getMethodName(), ".png"); + ParcelFileDescriptor bugreportFd2 = parcelFd(bugreportFile2); + ParcelFileDescriptor screenshotFd2 = parcelFd(screenshotFile2); mBrm.startBugreport(bugreportFd2, screenshotFd2, wifi(), mExecutor, callback2); Thread.sleep(500 /* .5s */); @@ -271,12 +291,16 @@ public class BugreportManagerTest { return bm; } - private static ParcelFileDescriptor parcelFd(String prefix, String extension) throws Exception { - File f = File.createTempFile(prefix, extension); + private static File createTempFile(String prefix, String extension) throws Exception { + final File f = File.createTempFile(prefix, extension); f.setReadable(true, true); f.setWritable(true, true); + f.deleteOnExit(); + return f; + } - return ParcelFileDescriptor.open(f, + private static ParcelFileDescriptor parcelFd(File file) throws Exception { + return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_APPEND); } @@ -342,4 +366,13 @@ public class BugreportManagerTest { private static BugreportParams interactive() { return new BugreportParams(BugreportParams.BUGREPORT_MODE_INTERACTIVE); } + + /* + * Returns a {@link BugreportParams} for full bugreport that includes a screenshot. + * + * <p> This can take on the order of minutes to finish + */ + private static BugreportParams full() { + return new BugreportParams(BugreportParams.BUGREPORT_MODE_FULL); + } } diff --git a/core/tests/coretests/src/android/view/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/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index f22222d10ad8..ff52a1abc6f4 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -663,13 +663,6 @@ public final class MediaRouter2 { if (sessionInfo == null) { notifyTransferFailure(requestedRoute); return; - } else if (!sessionInfo.getSelectedRoutes().contains(requestedRoute.getId())) { - Log.w(TAG, "The session does not contain the requested route. " - + "(requestedRouteId=" + requestedRoute.getId() - + ", actualRoutes=" + sessionInfo.getSelectedRoutes() - + ")"); - notifyTransferFailure(requestedRoute); - return; } else if (!TextUtils.equals(requestedRoute.getProviderId(), sessionInfo.getProviderId())) { Log.w(TAG, "The session's provider ID does not match the requested route's. " 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/bubbles/animation/PhysicsAnimationLayout.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java index 98a7cc23c67e..6e6f82b714ff 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java @@ -441,7 +441,10 @@ public class PhysicsAnimationLayout extends FrameLayout { // Cancel physics animations on the view. for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) { - getAnimationFromView(property, view).cancel(); + final DynamicAnimation animationFromView = getAnimationFromView(property, view); + if (animationFromView != null) { + animationFromView.cancel(); + } } } @@ -499,13 +502,13 @@ public class PhysicsAnimationLayout extends FrameLayout { * Retrieves the animation of the given property from the view at the given index via the view * tag system. */ - private SpringAnimation getAnimationAtIndex( + @Nullable private SpringAnimation getAnimationAtIndex( DynamicAnimation.ViewProperty property, int index) { return getAnimationFromView(property, getChildAt(index)); } /** Retrieves the animation of the given property from the view via the view tag system. */ - private SpringAnimation getAnimationFromView( + @Nullable private SpringAnimation getAnimationFromView( DynamicAnimation.ViewProperty property, View view) { return (SpringAnimation) view.getTag(getTagIdForProperty(property)); } @@ -536,8 +539,10 @@ public class PhysicsAnimationLayout extends FrameLayout { final float offset = mController.getOffsetForChainedPropertyAnimation(property); if (nextAnimInChain < getChildCount()) { - getAnimationAtIndex(property, nextAnimInChain) - .animateToFinalPosition(value + offset); + final SpringAnimation nextAnim = getAnimationAtIndex(property, nextAnimInChain); + if (nextAnim != null) { + nextAnim.animateToFinalPosition(value + offset); + } } }); diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt index e2215d57a094..68625059b2ef 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt @@ -2,6 +2,7 @@ package com.android.systemui.media import android.content.Context import android.content.Intent +import android.content.res.Configuration import android.graphics.Color import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS import android.view.LayoutInflater @@ -96,7 +97,6 @@ class MediaCarouselController @Inject constructor( * The measured height of the carousel */ private var carouselMeasureHeight: Int = 0 - private var playerWidthPlusPadding: Int = 0 private var desiredHostState: MediaHostState? = null private val mediaCarousel: MediaScrollView private val mediaCarouselScrollHandler: MediaCarouselScrollHandler @@ -108,6 +108,15 @@ class MediaCarouselController @Inject constructor( private val pageIndicator: PageIndicator private val visualStabilityCallback: VisualStabilityManager.Callback private var needsReordering: Boolean = false + private var isRtl: Boolean = false + set(value) { + if (value != field) { + field = value + mediaFrame.layoutDirection = + if (value) View.LAYOUT_DIRECTION_RTL else View.LAYOUT_DIRECTION_LTR + mediaCarouselScrollHandler.scrollToStart() + } + } private var currentlyExpanded = true set(value) { if (field != value) { @@ -126,6 +135,11 @@ class MediaCarouselController @Inject constructor( override fun onOverlayChanged() { inflateSettingsButton() } + + override fun onConfigChanged(newConfig: Configuration?) { + if (newConfig == null) return + isRtl = newConfig.layoutDirection == View.LAYOUT_DIRECTION_RTL + } } init { @@ -135,6 +149,7 @@ class MediaCarouselController @Inject constructor( mediaCarouselScrollHandler = MediaCarouselScrollHandler(mediaCarousel, pageIndicator, executor, mediaDataManager::onSwipeToDismiss, this::updatePageIndicatorLocation, falsingManager) + isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL inflateSettingsButton() mediaContent = mediaCarousel.requireViewById(R.id.media_carousel) configurationController.addCallback(configListener) @@ -144,7 +159,7 @@ class MediaCarouselController @Inject constructor( reorderAllPlayers() } // Let's reset our scroll position - mediaCarousel.scrollX = 0 + mediaCarouselScrollHandler.scrollToStart() } visualStabilityManager.addReorderingAllowedCallback(visualStabilityCallback, true /* persistent */) @@ -196,8 +211,13 @@ class MediaCarouselController @Inject constructor( } private fun inflateMediaCarousel(): ViewGroup { - return LayoutInflater.from(context).inflate(R.layout.media_carousel, + val mediaCarousel = LayoutInflater.from(context).inflate(R.layout.media_carousel, UniqueObjectHostView(context), false) as ViewGroup + // Because this is inflated when not attached to the true view hierarchy, it resolves some + // potential issues to force that the layout direction is defined by the locale + // (rather than inherited from the parent, which would resolve to LTR when unattached). + mediaCarousel.layoutDirection = View.LAYOUT_DIRECTION_LOCALE + return mediaCarousel } private fun reorderAllPlayers() { @@ -313,8 +333,12 @@ class MediaCarouselController @Inject constructor( private fun updatePageIndicatorLocation() { // Update the location of the page indicator, carousel clipping - pageIndicator.translationX = (currentCarouselWidth - pageIndicator.width) / 2.0f + - mediaCarouselScrollHandler.contentTranslation + val translationX = if (isRtl) { + (pageIndicator.width - currentCarouselWidth) / 2.0f + } else { + (currentCarouselWidth - pageIndicator.width) / 2.0f + } + pageIndicator.translationX = translationX + mediaCarouselScrollHandler.contentTranslation val layoutParams = pageIndicator.layoutParams as ViewGroup.MarginLayoutParams pageIndicator.translationY = (currentCarouselHeight - pageIndicator.height - layoutParams.bottomMargin).toFloat() @@ -334,7 +358,8 @@ class MediaCarouselController @Inject constructor( if (width != currentCarouselWidth || height != currentCarouselHeight) { currentCarouselWidth = width currentCarouselHeight = height - mediaCarouselScrollHandler.setCarouselBounds(currentCarouselWidth, currentCarouselHeight) + mediaCarouselScrollHandler.setCarouselBounds( + currentCarouselWidth, currentCarouselHeight) updatePageIndicatorLocation() } } @@ -348,7 +373,7 @@ class MediaCarouselController @Inject constructor( if (currentlyShowingOnlyActive != endShowsActive || ((currentTransitionProgress != 1.0f && currentTransitionProgress != 0.0f) && startShowsActive != endShowsActive)) { - /// Whenever we're transitioning from between differing states or the endstate differs + // Whenever we're transitioning from between differing states or the endstate differs // we reset the translation currentlyShowingOnlyActive = endShowsActive mediaCarouselScrollHandler.resetTranslation(animate = true) @@ -416,14 +441,15 @@ class MediaCarouselController @Inject constructor( height != carouselMeasureWidth && height != 0) { carouselMeasureWidth = width carouselMeasureHeight = height - playerWidthPlusPadding = carouselMeasureWidth + context.resources.getDimensionPixelSize( - R.dimen.qs_media_padding) - mediaCarouselScrollHandler.playerWidthPlusPadding = playerWidthPlusPadding + val playerWidthPlusPadding = carouselMeasureWidth + + context.resources.getDimensionPixelSize(R.dimen.qs_media_padding) // Let's remeasure the carousel val widthSpec = desiredHostState?.measurementInput?.widthMeasureSpec ?: 0 val heightSpec = desiredHostState?.measurementInput?.heightMeasureSpec ?: 0 mediaCarousel.measure(widthSpec, heightSpec) mediaCarousel.layout(0, 0, width, mediaCarousel.measuredHeight) + // Update the padding after layout; view widths are used in RTL to calculate scrollX + mediaCarouselScrollHandler.playerWidthPlusPadding = playerWidthPlusPadding } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt index 993c05fbbd6f..ef2f71100e1a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt @@ -59,6 +59,10 @@ class MediaCarouselScrollHandler( private val falsingManager: FalsingManager ) { /** + * Is the view in RTL + */ + val isRtl: Boolean get() = scrollView.isLayoutRtl + /** * Do we need falsing protection? */ var falsingProtectionNeeded: Boolean = false @@ -121,14 +125,14 @@ class MediaCarouselScrollHandler( field = value // The player width has changed, let's update the scroll position to make sure // it's still at the same place - var newScroll = activeMediaIndex * playerWidthPlusPadding + var newRelativeScroll = activeMediaIndex * playerWidthPlusPadding if (scrollIntoCurrentMedia > playerWidthPlusPadding) { - newScroll += playerWidthPlusPadding - + newRelativeScroll += playerWidthPlusPadding - (scrollIntoCurrentMedia - playerWidthPlusPadding) } else { - newScroll += scrollIntoCurrentMedia + newRelativeScroll += scrollIntoCurrentMedia } - scrollView.scrollX = newScroll + scrollView.relativeScrollX = newRelativeScroll } /** @@ -184,8 +188,9 @@ class MediaCarouselScrollHandler( if (playerWidthPlusPadding == 0) { return } - onMediaScrollingChanged(scrollX / playerWidthPlusPadding, - scrollX % playerWidthPlusPadding) + val relativeScrollX = scrollView.relativeScrollX + onMediaScrollingChanged(relativeScrollX / playerWidthPlusPadding, + relativeScrollX % playerWidthPlusPadding) } } @@ -222,11 +227,19 @@ class MediaCarouselScrollHandler( Math.abs(contentTranslation)) val settingsTranslation = (1.0f - settingsOffset) * -settingsButton.width * SETTINGS_BUTTON_TRANSLATION_FRACTION - val newTranslationX: Float - if (contentTranslation > 0) { - newTranslationX = settingsTranslation + val newTranslationX = if (isRtl) { + // In RTL, the 0-placement is on the right side of the view, not the left... + if (contentTranslation > 0) { + -(scrollView.width - settingsTranslation - settingsButton.width) + } else { + -settingsTranslation + } } else { - newTranslationX = scrollView.width - settingsTranslation - settingsButton.width + if (contentTranslation > 0) { + settingsTranslation + } else { + scrollView.width - settingsTranslation - settingsButton.width + } } val rotation = (1.0f - settingsOffset) * 50 settingsButton.rotation = rotation * -Math.signum(contentTranslation) @@ -259,26 +272,26 @@ class MediaCarouselScrollHandler( } if (isUp || motionEvent.action == MotionEvent.ACTION_CANCEL) { // It's an up and the fling didn't take it above - val pos = scrollView.scrollX % playerWidthPlusPadding - val scollXAmount: Int - if (pos > playerWidthPlusPadding / 2) { - scollXAmount = playerWidthPlusPadding - pos + val relativePos = scrollView.relativeScrollX % playerWidthPlusPadding + val scrollXAmount: Int + if (relativePos > playerWidthPlusPadding / 2) { + scrollXAmount = playerWidthPlusPadding - relativePos } else { - scollXAmount = -1 * pos + scrollXAmount = -1 * relativePos } - if (scollXAmount != 0) { + if (scrollXAmount != 0) { // Delay the scrolling since scrollView calls springback which cancels // the animation again.. mainExecutor.execute { - scrollView.smoothScrollBy(scollXAmount, 0) + scrollView.smoothScrollBy(if (isRtl) -scrollXAmount else scrollXAmount, 0) } } val currentTranslation = scrollView.getContentTranslation() if (currentTranslation != 0.0f) { // We started a Swipe but didn't end up with a fling. Let's either go to the // dismissed position or go back. - val springBack = Math.abs(currentTranslation) < getMaxTranslation() / 2 - || isFalseTouch() + val springBack = Math.abs(currentTranslation) < getMaxTranslation() / 2 || + isFalseTouch() val newTranslation: Float if (springBack) { newTranslation = 0.0f @@ -313,9 +326,11 @@ class MediaCarouselScrollHandler( return gestureDetector.onTouchEvent(motionEvent) } - fun onScroll(down: MotionEvent, - lastMotion: MotionEvent, - distanceX: Float): Boolean { + fun onScroll( + down: MotionEvent, + lastMotion: MotionEvent, + distanceX: Float + ): Boolean { val totalX = lastMotion.x - down.x val currentTranslation = scrollView.getContentTranslation() if (currentTranslation != 0.0f || @@ -339,8 +354,8 @@ class MediaCarouselScrollHandler( } // Otherwise we don't have do do anything, and will remove the unrubberbanded // translation } - if (Math.signum(newTranslation) != Math.signum(currentTranslation) - && currentTranslation != 0.0f) { + if (Math.signum(newTranslation) != Math.signum(currentTranslation) && + currentTranslation != 0.0f) { // We crossed the 0.0 threshold of the translation. Let's see if we're allowed // to scroll into the new direction if (scrollView.canScrollHorizontally(-newTranslation.toInt())) { @@ -394,9 +409,10 @@ class MediaCarouselScrollHandler( scrollView.animationTargetX = newTranslation } else { // We're flinging the player! Let's go either to the previous or to the next player - val pos = scrollView.scrollX + val pos = scrollView.relativeScrollX val currentIndex = if (playerWidthPlusPadding > 0) pos / playerWidthPlusPadding else 0 - var destIndex = if (vX <= 0) currentIndex + 1 else currentIndex + val flungTowardEnd = if (isRtl) vX > 0 else vX < 0 + var destIndex = if (flungTowardEnd) currentIndex + 1 else currentIndex destIndex = Math.max(0, destIndex) destIndex = Math.min(mediaContent.getChildCount() - 1, destIndex) val view = mediaContent.getChildAt(destIndex) @@ -438,8 +454,14 @@ class MediaCarouselScrollHandler( activeMediaIndex = newIndex updatePlayerVisibilities() } - val location = activeMediaIndex.toFloat() + if (playerWidthPlusPadding > 0) + val relativeLocation = activeMediaIndex.toFloat() + if (playerWidthPlusPadding > 0) scrollInAmount.toFloat() / playerWidthPlusPadding else 0f + // Fix the location, because PageIndicator does not handle RTL internally + val location = if (isRtl) { + mediaContent.childCount - relativeLocation - 1 + } else { + relativeLocation + } pageIndicator.setLocation(location) updateClipToOutline() } @@ -480,13 +502,20 @@ class MediaCarouselScrollHandler( * where it was and update our scroll position. */ fun onPrePlayerRemoved(removed: MediaControlPanel) { - val beforeActive = mediaContent.indexOfChild(removed.view?.player) <= activeMediaIndex + val removedIndex = mediaContent.indexOfChild(removed.view?.player) + // If the removed index is less than the activeMediaIndex, then we need to decrement it. + // RTL has no effect on this, because indices are always relative (start-to-end). + // Update the index 'manually' since we won't always get a call to onMediaScrollingChanged + val beforeActive = removedIndex <= activeMediaIndex if (beforeActive) { - // also update the index here since the scroll below might not always lead - // to a scrolling changed activeMediaIndex = Math.max(0, activeMediaIndex - 1) - scrollView.scrollX = Math.max(scrollView.scrollX - - playerWidthPlusPadding, 0) + } + // If the removed media item is "left of" the active one (in an absolute sense), we need to + // scroll the view to keep that player in view. This is because scroll position is always + // calculated from left to right. + val leftOfActive = if (isRtl) !beforeActive else beforeActive + if (leftOfActive) { + scrollView.scrollX = Math.max(scrollView.scrollX - playerWidthPlusPadding, 0) } } @@ -501,6 +530,13 @@ class MediaCarouselScrollHandler( } } + /** + * Reset the MediaScrollView to the start. + */ + fun scrollToStart() { + scrollView.relativeScrollX = 0 + } + companion object { private val CONTENT_TRANSLATION = object : FloatPropertyCompat<MediaCarouselScrollHandler>( "contentTranslation") { diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt b/packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt index a079b06a0b10..b8872250bb6c 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt @@ -15,7 +15,10 @@ import com.android.systemui.util.animation.physicsAnimator * when only measuring children but not the parent, when trying to apply a new scroll position */ class MediaScrollView @JvmOverloads constructor( - context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : HorizontalScrollView(context, attrs, defStyleAttr) { lateinit var contentContainer: ViewGroup @@ -38,6 +41,26 @@ class MediaScrollView @JvmOverloads constructor( } /** + * Convert between the absolute (left-to-right) and relative (start-to-end) scrollX of the media + * carousel. The player indices are always relative (start-to-end) and the scrollView.scrollX + * is always absolute. This function is its own inverse. + */ + private fun transformScrollX(scrollX: Int): Int = if (isLayoutRtl) { + contentContainer.width - width - scrollX + } else { + scrollX + } + + /** + * Get the layoutDirection-relative (start-to-end) scroll X position of the carousel. + */ + var relativeScrollX: Int + get() = transformScrollX(scrollX) + set(value) { + scrollX = transformScrollX(value) + } + + /** * Allow all scrolls to go through, use base implementation */ override fun scrollTo(x: Int, y: Int) { @@ -55,15 +78,15 @@ class MediaScrollView @JvmOverloads constructor( } override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { - var intercept = false; + var intercept = false touchListener?.let { intercept = it.onInterceptTouchEvent(ev) } - return super.onInterceptTouchEvent(ev) || intercept; + return super.onInterceptTouchEvent(ev) || intercept } override fun onTouchEvent(ev: MotionEvent?): Boolean { - var touch = false; + var touch = false touchListener?.let { touch = it.onTouchEvent(ev) } @@ -75,9 +98,17 @@ class MediaScrollView @JvmOverloads constructor( contentContainer = getChildAt(0) as ViewGroup } - override fun overScrollBy(deltaX: Int, deltaY: Int, scrollX: Int, scrollY: Int, - scrollRangeX: Int, scrollRangeY: Int, maxOverScrollX: Int, - maxOverScrollY: Int, isTouchEvent: Boolean): Boolean { + override fun overScrollBy( + deltaX: Int, + deltaY: Int, + scrollX: Int, + scrollY: Int, + scrollRangeX: Int, + scrollRangeY: Int, + maxOverScrollX: Int, + maxOverScrollY: Int, + isTouchEvent: Boolean + ): Boolean { if (getContentTranslation() != 0.0f) { // When we're dismissing we ignore all the scrolling return false diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMotionHelper.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMotionHelper.java index 26805050e841..e60123e863a4 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMotionHelper.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMotionHelper.java @@ -365,6 +365,10 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, void flingToSnapTarget( float velocityX, float velocityY, @Nullable Runnable updateAction, @Nullable Runnable endAction) { + // If we're flinging to a snap target now, we're not springing to catch up to the touch + // location now. + mSpringingToTouch = false; + mTemporaryBoundsPhysicsAnimator .spring(FloatProperties.RECT_WIDTH, mBounds.width(), mSpringConfig) .spring(FloatProperties.RECT_HEIGHT, mBounds.height(), mSpringConfig) diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java index e66b33c660d6..9dcc924f161e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java @@ -64,6 +64,8 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha private TouchAnimator mTranslationYAnimator; private TouchAnimator mNonfirstPageAnimator; private TouchAnimator mNonfirstPageDelayedAnimator; + // This animates fading of SecurityFooter and media divider + private TouchAnimator mAllPagesDelayedAnimator; private TouchAnimator mBrightnessAnimator; private boolean mNeedsAnimatorUpdate = false; @@ -296,19 +298,24 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha Builder builder = new Builder() .setStartDelay(EXPANDED_TILE_DELAY) .addFloat(tileLayout, "alpha", 0, 1); + mFirstPageDelayedAnimator = builder.build(); + + // Fade in the security footer and the divider as we reach the final position + builder = new Builder().setStartDelay(EXPANDED_TILE_DELAY); if (mQsPanel.getSecurityFooter() != null) { builder.addFloat(mQsPanel.getSecurityFooter().getView(), "alpha", 0, 1); } if (mQsPanel.getDivider() != null) { builder.addFloat(mQsPanel.getDivider(), "alpha", 0, 1); } - mFirstPageDelayedAnimator = builder.build(); + mAllPagesDelayedAnimator = builder.build(); if (mQsPanel.getSecurityFooter() != null) { mAllViews.add(mQsPanel.getSecurityFooter().getView()); } if (mQsPanel.getDivider() != null) { mAllViews.add(mQsPanel.getDivider()); } + float px = 0; float py = 1; if (tiles.size() <= 3) { @@ -388,6 +395,9 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha mNonfirstPageAnimator.setPosition(position); mNonfirstPageDelayedAnimator.setPosition(position); } + if (mAllowFancy) { + mAllPagesDelayedAnimator.setPosition(position); + } } @Override diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java index 32ef063a55be..0c34b27d348e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java @@ -22,13 +22,13 @@ import android.text.TextUtils; import android.util.Log; import android.widget.Switch; -import com.android.internal.logging.UiEventLogger; import com.android.systemui.R; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.qs.QSHost; import com.android.systemui.qs.tileimpl.QSTileImpl; import com.android.systemui.screenrecord.RecordingController; +import com.android.systemui.statusbar.phone.KeyguardDismissUtil; import javax.inject.Inject; @@ -39,19 +39,17 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState> implements RecordingController.RecordingStateChangeCallback { private static final String TAG = "ScreenRecordTile"; private RecordingController mController; - private ActivityStarter mActivityStarter; + private KeyguardDismissUtil mKeyguardDismissUtil; private long mMillisUntilFinished = 0; private Callback mCallback = new Callback(); - private UiEventLogger mUiEventLogger; @Inject public ScreenRecordTile(QSHost host, RecordingController controller, - ActivityStarter activityStarter, UiEventLogger uiEventLogger) { + KeyguardDismissUtil keyguardDismissUtil) { super(host); mController = controller; mController.observe(this, mCallback); - mActivityStarter = activityStarter; - mUiEventLogger = uiEventLogger; + mKeyguardDismissUtil = keyguardDismissUtil; } @Override @@ -69,7 +67,7 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState> } else if (mController.isRecording()) { stopRecording(); } else { - startCountdown(); + mUiHandler.post(() -> showPrompt()); } refreshState(); } @@ -114,11 +112,15 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState> return mContext.getString(R.string.quick_settings_screen_record_label); } - private void startCountdown() { - // Close QS, otherwise the permission dialog appears beneath it + private void showPrompt() { + // Close QS, otherwise the dialog appears beneath it getHost().collapsePanels(); Intent intent = mController.getPromptIntent(); - mActivityStarter.postStartActivityDismissingKeyguard(intent, 0); + ActivityStarter.OnDismissAction dismissAction = () -> { + mContext.startActivity(intent); + return false; + }; + mKeyguardDismissUtil.executeWhenUnlocked(dismissAction, false); } private void cancelCountdown() { diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java index b253635e9bfa..82ac1f6f6a33 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java @@ -17,12 +17,17 @@ package com.android.systemui.screenrecord; import android.app.PendingIntent; +import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.os.CountDownTimer; +import android.os.UserHandle; import android.util.Log; +import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.statusbar.policy.CallbackController; import java.util.ArrayList; @@ -41,21 +46,30 @@ public class RecordingController private static final String SYSUI_SCREENRECORD_LAUNCHER = "com.android.systemui.screenrecord.ScreenRecordDialog"; - private final Context mContext; private boolean mIsStarting; private boolean mIsRecording; private PendingIntent mStopIntent; private CountDownTimer mCountDownTimer = null; + private BroadcastDispatcher mBroadcastDispatcher; private ArrayList<RecordingStateChangeCallback> mListeners = new ArrayList<>(); + @VisibleForTesting + protected final BroadcastReceiver mUserChangeReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (mStopIntent != null) { + stopRecording(); + } + } + }; + /** * Create a new RecordingController - * @param context Context for the controller */ @Inject - public RecordingController(Context context) { - mContext = context; + public RecordingController(BroadcastDispatcher broadcastDispatcher) { + mBroadcastDispatcher = broadcastDispatcher; } /** @@ -99,6 +113,9 @@ public class RecordingController } try { startIntent.send(); + IntentFilter userFilter = new IntentFilter(Intent.ACTION_USER_SWITCHED); + mBroadcastDispatcher.registerReceiver(mUserChangeReceiver, userFilter, null, + UserHandle.ALL); Log.d(TAG, "sent start intent"); } catch (PendingIntent.CanceledException e) { Log.e(TAG, "Pending intent was cancelled: " + e.getMessage()); @@ -146,11 +163,16 @@ public class RecordingController */ public void stopRecording() { try { - mStopIntent.send(); + if (mStopIntent != null) { + mStopIntent.send(); + } else { + Log.e(TAG, "Stop intent was null"); + } updateState(false); } catch (PendingIntent.CanceledException e) { Log.e(TAG, "Error stopping: " + e.getMessage()); } + mBroadcastDispatcher.unregisterReceiver(mUserChangeReceiver); } /** diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java index 87597263168a..476ec798a35f 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java @@ -32,6 +32,7 @@ import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; +import android.os.UserHandle; import android.provider.Settings; import android.util.Log; import android.widget.Toast; @@ -40,6 +41,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.UiEventLogger; import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.LongRunning; +import com.android.systemui.settings.CurrentUserContextTracker; import java.io.IOException; import java.util.concurrent.Executor; @@ -58,7 +60,6 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis private static final String TAG = "RecordingService"; private static final String CHANNEL_ID = "screen_record"; private static final String EXTRA_RESULT_CODE = "extra_resultCode"; - private static final String EXTRA_DATA = "extra_data"; private static final String EXTRA_PATH = "extra_path"; private static final String EXTRA_AUDIO_SOURCE = "extra_useAudio"; private static final String EXTRA_SHOW_TAPS = "extra_showTaps"; @@ -79,14 +80,17 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis private final Executor mLongExecutor; private final UiEventLogger mUiEventLogger; private final NotificationManager mNotificationManager; + private final CurrentUserContextTracker mUserContextTracker; @Inject public RecordingService(RecordingController controller, @LongRunning Executor executor, - UiEventLogger uiEventLogger, NotificationManager notificationManager) { + UiEventLogger uiEventLogger, NotificationManager notificationManager, + CurrentUserContextTracker userContextTracker) { mController = controller; mLongExecutor = executor; mUiEventLogger = uiEventLogger; mNotificationManager = notificationManager; + mUserContextTracker = userContextTracker; } /** @@ -95,8 +99,6 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis * @param context Context from the requesting activity * @param resultCode The result code from {@link android.app.Activity#onActivityResult(int, int, * android.content.Intent)} - * @param data The data from {@link android.app.Activity#onActivityResult(int, int, - * android.content.Intent)} * @param audioSource The ordinal value of the audio source * {@link com.android.systemui.screenrecord.ScreenRecordingAudioSource} * @param showTaps True to make touches visible while recording @@ -118,6 +120,8 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis String action = intent.getAction(); Log.d(TAG, "onStartCommand " + action); + int mCurrentUserId = mUserContextTracker.getCurrentUserContext().getUserId(); + UserHandle currentUser = new UserHandle(mCurrentUserId); switch (action) { case ACTION_START: mAudioSource = ScreenRecordingAudioSource @@ -132,8 +136,8 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis setTapsVisible(mShowTaps); mRecorder = new ScreenMediaRecorder( - getApplicationContext(), - getUserId(), + mUserContextTracker.getCurrentUserContext(), + mCurrentUserId, mAudioSource, this ); @@ -148,7 +152,14 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis } else { mUiEventLogger.log(Events.ScreenRecordEvent.SCREEN_RECORD_END_QS_TILE); } - stopRecording(); + // Check user ID - we may be getting a stop intent after user switch, in which case + // we want to post the notifications for that user, which is NOT current user + int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); + if (userId == -1) { + userId = mUserContextTracker.getCurrentUserContext().getUserId(); + } + Log.d(TAG, "notifying for user " + userId); + stopRecording(userId); mNotificationManager.cancel(NOTIFICATION_RECORDING_ID); stopSelf(); break; @@ -165,7 +176,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); // Remove notification - mNotificationManager.cancel(NOTIFICATION_VIEW_ID); + mNotificationManager.cancelAsUser(null, NOTIFICATION_VIEW_ID, currentUser); startActivity(Intent.createChooser(shareIntent, shareLabel) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); @@ -184,7 +195,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis Toast.LENGTH_LONG).show(); // Remove notification - mNotificationManager.cancel(NOTIFICATION_VIEW_ID); + mNotificationManager.cancelAsUser(null, NOTIFICATION_VIEW_ID, currentUser); Log.d(TAG, "Deleted recording " + uri); break; } @@ -215,11 +226,12 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis mController.updateState(true); createRecordingNotification(); mUiEventLogger.log(Events.ScreenRecordEvent.SCREEN_RECORD_START); - } catch (IOException | RemoteException e) { + } catch (IOException | RemoteException | IllegalStateException e) { Toast.makeText(this, R.string.screenrecord_start_error, Toast.LENGTH_LONG) .show(); e.printStackTrace(); + mController.updateState(false); } } @@ -242,7 +254,6 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis ? res.getString(R.string.screenrecord_ongoing_screen_only) : res.getString(R.string.screenrecord_ongoing_screen_and_audio); - Intent stopIntent = getNotificationIntent(this); Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID) .setSmallIcon(R.drawable.ic_screenrecord) @@ -254,7 +265,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis .setOngoing(true) .setContentIntent( PendingIntent.getService(this, REQUEST_CODE, stopIntent, - PendingIntent.FLAG_UPDATE_CURRENT)) + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)) .addExtras(extras); startForeground(NOTIFICATION_RECORDING_ID, builder.build()); } @@ -265,11 +276,17 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis String notificationTitle = mAudioSource == ScreenRecordingAudioSource.NONE ? res.getString(R.string.screenrecord_ongoing_screen_only) : res.getString(R.string.screenrecord_ongoing_screen_and_audio); + + Bundle extras = new Bundle(); + extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, + res.getString(R.string.screenrecord_name)); + Notification.Builder builder = new Notification.Builder(getApplicationContext(), CHANNEL_ID) .setContentTitle(notificationTitle) .setContentText( getResources().getString(R.string.screenrecord_background_processing_label)) - .setSmallIcon(R.drawable.ic_screenrecord); + .setSmallIcon(R.drawable.ic_screenrecord) + .addExtras(extras); return builder.build(); } @@ -287,7 +304,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis this, REQUEST_CODE, getShareIntent(this, uri.toString()), - PendingIntent.FLAG_UPDATE_CURRENT)) + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)) .build(); Notification.Action deleteAction = new Notification.Action.Builder( @@ -297,7 +314,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis this, REQUEST_CODE, getDeleteIntent(this, uri.toString()), - PendingIntent.FLAG_UPDATE_CURRENT)) + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)) .build(); Bundle extras = new Bundle(); @@ -328,34 +345,36 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis return builder.build(); } - private void stopRecording() { + private void stopRecording(int userId) { setTapsVisible(mOriginalShowTaps); if (getRecorder() != null) { getRecorder().end(); - saveRecording(); + saveRecording(userId); } else { Log.e(TAG, "stopRecording called, but recorder was null"); } mController.updateState(false); } - private void saveRecording() { - mNotificationManager.notify(NOTIFICATION_PROCESSING_ID, createProcessingNotification()); + private void saveRecording(int userId) { + UserHandle currentUser = new UserHandle(userId); + mNotificationManager.notifyAsUser(null, NOTIFICATION_PROCESSING_ID, + createProcessingNotification(), currentUser); mLongExecutor.execute(() -> { try { Log.d(TAG, "saving recording"); Notification notification = createSaveNotification(getRecorder().save()); if (!mController.isRecording()) { - Log.d(TAG, "showing saved notification"); - mNotificationManager.notify(NOTIFICATION_VIEW_ID, notification); + mNotificationManager.notifyAsUser(null, NOTIFICATION_VIEW_ID, notification, + currentUser); } } catch (IOException e) { Log.e(TAG, "Error saving screen recording: " + e.getMessage()); Toast.makeText(this, R.string.screenrecord_delete_error, Toast.LENGTH_LONG) .show(); } finally { - mNotificationManager.cancel(NOTIFICATION_PROCESSING_ID); + mNotificationManager.cancelAsUser(null, NOTIFICATION_PROCESSING_ID, currentUser); } }); } @@ -371,7 +390,9 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis * @return */ public static Intent getStopIntent(Context context) { - return new Intent(context, RecordingService.class).setAction(ACTION_STOP); + return new Intent(context, RecordingService.class) + .setAction(ACTION_STOP) + .putExtra(Intent.EXTRA_USER_HANDLE, context.getUserId()); } /** diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenInternalAudioRecorder.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenInternalAudioRecorder.java index edbc3cfdece5..df03c3e08f08 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenInternalAudioRecorder.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenInternalAudioRecorder.java @@ -16,7 +16,6 @@ package com.android.systemui.screenrecord; -import android.content.Context; import android.media.AudioAttributes; import android.media.AudioFormat; import android.media.AudioPlaybackCaptureConfiguration; @@ -39,7 +38,6 @@ public class ScreenInternalAudioRecorder { private static String TAG = "ScreenAudioRecorder"; private static final int TIMEOUT = 500; private static final float MIC_VOLUME_SCALE = 1.4f; - private final Context mContext; private AudioRecord mAudioRecord; private AudioRecord mAudioRecordMic; private Config mConfig = new Config(); @@ -49,17 +47,14 @@ public class ScreenInternalAudioRecorder { private long mPresentationTime; private long mTotalBytes; private MediaMuxer mMuxer; - private String mOutFile; private boolean mMic; private int mTrackId = -1; - public ScreenInternalAudioRecorder(String outFile, Context context, - MediaProjection mp, boolean includeMicInput) throws IOException { + public ScreenInternalAudioRecorder(String outFile, MediaProjection mp, boolean includeMicInput) + throws IOException { mMic = includeMicInput; - mOutFile = outFile; mMuxer = new MediaMuxer(outFile, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); - mContext = context; mMediaProjection = mp; Log.d(TAG, "creating audio file " + outFile); setupSimple(); @@ -266,8 +261,9 @@ public class ScreenInternalAudioRecorder { /** * start recording + * @throws IllegalStateException if recording fails to initialize */ - public void start() { + public void start() throws IllegalStateException { if (mThread != null) { Log.e(TAG, "a recording is being done in parallel or stop is not called"); } @@ -276,8 +272,7 @@ public class ScreenInternalAudioRecorder { Log.d(TAG, "channel count " + mAudioRecord.getChannelCount()); mCodec.start(); if (mAudioRecord.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) { - Log.e(TAG, "Error starting audio recording"); - return; + throw new IllegalStateException("Audio recording failed to start"); } mThread.start(); } diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java index 1c7d987afff2..1a9abb9cf27d 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java @@ -166,7 +166,7 @@ public class ScreenMediaRecorder { mAudioSource == MIC_AND_INTERNAL) { mTempAudioFile = File.createTempFile("temp", ".aac", mContext.getCacheDir()); - mAudio = new ScreenInternalAudioRecorder(mTempAudioFile.getAbsolutePath(), mContext, + mAudio = new ScreenInternalAudioRecorder(mTempAudioFile.getAbsolutePath(), mMediaProjection, mAudioSource == MIC_AND_INTERNAL); } @@ -175,7 +175,7 @@ public class ScreenMediaRecorder { /** * Start screen recording */ - void start() throws IOException, RemoteException { + void start() throws IOException, RemoteException, IllegalStateException { Log.d(TAG, "start recording"); prepare(); mMediaRecorder.start(); @@ -205,7 +205,7 @@ public class ScreenMediaRecorder { } } - private void recordInternalAudio() { + private void recordInternalAudio() throws IllegalStateException { if (mAudioSource == INTERNAL || mAudioSource == MIC_AND_INTERNAL) { mAudio.start(); } diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java index 8347def2d430..dc47ab4dff63 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java @@ -23,16 +23,19 @@ import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.NONE; import android.app.Activity; import android.app.PendingIntent; +import android.content.Context; import android.os.Bundle; import android.view.Gravity; import android.view.ViewGroup; import android.view.Window; +import android.view.WindowManager; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.Spinner; import android.widget.Switch; import com.android.systemui.R; +import com.android.systemui.settings.CurrentUserContextTracker; import java.util.ArrayList; import java.util.List; @@ -48,16 +51,17 @@ public class ScreenRecordDialog extends Activity { private static final String TAG = "ScreenRecordDialog"; private final RecordingController mController; + private final CurrentUserContextTracker mCurrentUserContextTracker; private Switch mTapsSwitch; private Switch mAudioSwitch; private Spinner mOptions; private List<ScreenRecordingAudioSource> mModes; - private int mSelected; - @Inject - public ScreenRecordDialog(RecordingController controller) { + public ScreenRecordDialog(RecordingController controller, + CurrentUserContextTracker currentUserContextTracker) { mController = controller; + mCurrentUserContextTracker = currentUserContextTracker; } @Override @@ -68,6 +72,7 @@ public class ScreenRecordDialog extends Activity { // Inflate the decor view, so the attributes below are not overwritten by the theme. window.getDecorView(); window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + window.addPrivateFlags(WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS); window.setGravity(Gravity.TOP); setTitle(R.string.screenrecord_name); @@ -100,24 +105,24 @@ public class ScreenRecordDialog extends Activity { mOptions.setOnItemClickListenerInt((parent, view, position, id) -> { mAudioSwitch.setChecked(true); }); - } private void requestScreenCapture() { + Context userContext = mCurrentUserContextTracker.getCurrentUserContext(); boolean showTaps = mTapsSwitch.isChecked(); ScreenRecordingAudioSource audioMode = mAudioSwitch.isChecked() ? (ScreenRecordingAudioSource) mOptions.getSelectedItem() : NONE; - PendingIntent startIntent = PendingIntent.getForegroundService(this, + PendingIntent startIntent = PendingIntent.getForegroundService(userContext, RecordingService.REQUEST_CODE, RecordingService.getStartIntent( - ScreenRecordDialog.this, RESULT_OK, + userContext, RESULT_OK, audioMode.ordinal(), showTaps), - PendingIntent.FLAG_UPDATE_CURRENT); - PendingIntent stopIntent = PendingIntent.getService(this, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + PendingIntent stopIntent = PendingIntent.getService(userContext, RecordingService.REQUEST_CODE, - RecordingService.getStopIntent(this), - PendingIntent.FLAG_UPDATE_CURRENT); + RecordingService.getStopIntent(userContext), + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); mController.startCountdown(DELAY_MS, INTERVAL_MS, startIntent, stopIntent); } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java index 9bbc4ddcc62c..8e878ddc6da1 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java @@ -58,7 +58,6 @@ import android.os.Looper; import android.os.Message; import android.os.PowerManager; import android.os.RemoteException; -import android.os.UserHandle; import android.provider.Settings; import android.util.DisplayMetrics; import android.util.Log; @@ -1179,11 +1178,15 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset @Override public void onReceive(Context context, Intent intent) { PendingIntent pendingIntent = intent.getParcelableExtra(EXTRA_ACTION_INTENT); - Intent actionIntent = pendingIntent.getIntent(); String actionType = intent.getStringExtra(EXTRA_ACTION_TYPE); - Slog.d(TAG, "Executing smart action [" + actionType + "]:" + actionIntent); + Slog.d(TAG, "Executing smart action [" + actionType + "]:" + pendingIntent.getIntent()); ActivityOptions opts = ActivityOptions.makeBasic(); - context.startActivityAsUser(actionIntent, opts.toBundle(), UserHandle.CURRENT); + + try { + pendingIntent.send(context, 0, null, null, null, null, opts.toBundle()); + } catch (PendingIntent.CanceledException e) { + Log.e(TAG, "Pending intent canceled", e); + } ScreenshotSmartActions.notifyScreenshotAction( context, intent.getStringExtra(EXTRA_ID), actionType, true); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java index 26ccd721460e..bb78f6168d28 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java @@ -77,6 +77,7 @@ public abstract class ExpandableOutlineView extends ExpandableView { protected boolean mShouldTranslateContents; private boolean mTopAmountRounded; private float mDistanceToTopRoundness = -1; + private float[] mTmpCornerRadii = new float[8]; private final ViewOutlineProvider mProvider = new ViewOutlineProvider() { @Override @@ -138,38 +139,22 @@ public abstract class ExpandableOutlineView extends ExpandableView { bottomRoundness -= overShoot * mCurrentBottomRoundness / (mCurrentTopRoundness + mCurrentBottomRoundness); } - getRoundedRectPath(left, top, right, bottom, topRoundness, - bottomRoundness, mTmpPath); + getRoundedRectPath(left, top, right, bottom, topRoundness, bottomRoundness, mTmpPath); return mTmpPath; } - public static void getRoundedRectPath(int left, int top, int right, int bottom, + public void getRoundedRectPath(int left, int top, int right, int bottom, float topRoundness, float bottomRoundness, Path outPath) { outPath.reset(); - int width = right - left; - float topRoundnessX = topRoundness; - float bottomRoundnessX = bottomRoundness; - topRoundnessX = Math.min(width / 2, topRoundnessX); - bottomRoundnessX = Math.min(width / 2, bottomRoundnessX); - if (topRoundness > 0.0f) { - outPath.moveTo(left, top + topRoundness); - outPath.quadTo(left, top, left + topRoundnessX, top); - outPath.lineTo(right - topRoundnessX, top); - outPath.quadTo(right, top, right, top + topRoundness); - } else { - outPath.moveTo(left, top); - outPath.lineTo(right, top); - } - if (bottomRoundness > 0.0f) { - outPath.lineTo(right, bottom - bottomRoundness); - outPath.quadTo(right, bottom, right - bottomRoundnessX, bottom); - outPath.lineTo(left + bottomRoundnessX, bottom); - outPath.quadTo(left, bottom, left, bottom - bottomRoundness); - } else { - outPath.lineTo(right, bottom); - outPath.lineTo(left, bottom); - } - outPath.close(); + mTmpCornerRadii[0] = topRoundness; + mTmpCornerRadii[1] = topRoundness; + mTmpCornerRadii[2] = topRoundness; + mTmpCornerRadii[3] = topRoundness; + mTmpCornerRadii[4] = bottomRoundness; + mTmpCornerRadii[5] = bottomRoundness; + mTmpCornerRadii[6] = bottomRoundness; + mTmpCornerRadii[7] = bottomRoundness; + outPath.addRoundRect(left, top, right, bottom, mTmpCornerRadii, Path.Direction.CW); } public ExpandableOutlineView(Context context, AttributeSet attrs) { @@ -188,9 +173,7 @@ public abstract class ExpandableOutlineView extends ExpandableView { int right = getWidth() + (int) (mExtraWidthForClipping + left); int bottom = (int) Math.max(mMinimumHeightForClipping, Math.max(getActualHeight() - mClipBottomAmount, top + mOutlineRadius)); - ExpandableOutlineView.getRoundedRectPath(left, top, right, bottom, mOutlineRadius, - 0.0f, - mClipPath); + getRoundedRectPath(left, top, right, bottom, mOutlineRadius, 0.0f, mClipPath); intersectPath = mClipPath; } boolean clipped = false; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java index b0861bfbd643..1f5b063b0aa2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java @@ -660,6 +660,14 @@ public class NotificationContentView extends FrameLayout { private void updateContentTransformation() { int visibleType = calculateVisibleType(); + if (getTransformableViewForVisibleType(mVisibleType) == null) { + // Case where visible view was removed in middle of transformation. In this case, we + // just update immediately to the appropriate view. + mVisibleType = visibleType; + updateViewVisibilities(visibleType); + updateBackgroundColor(false); + return; + } if (visibleType != mVisibleType) { // A new transformation starts mTransformationStartVisibleType = mVisibleType; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java index f2eec39ed17e..c32133119386 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java @@ -166,6 +166,7 @@ public class NotificationPanelViewController extends PanelViewController { private final ConfigurationListener mConfigurationListener = new ConfigurationListener(); private final StatusBarStateListener mStatusBarStateListener = new StatusBarStateListener(); private final ExpansionCallback mExpansionCallback = new ExpansionCallback(); + private final BiometricUnlockController mBiometricUnlockController; private final NotificationPanelView mView; private final MetricsLogger mMetricsLogger; private final ActivityManager mActivityManager; @@ -227,7 +228,8 @@ public class NotificationPanelViewController extends PanelViewController { mBarState == StatusBarState.KEYGUARD || mBarState == StatusBarState.SHADE_LOCKED; if (!running && mFirstBypassAttempt && keyguardOrShadeLocked && !mDozing - && !mDelayShowingKeyguardStatusBar) { + && !mDelayShowingKeyguardStatusBar + && !mBiometricUnlockController.isBiometricUnlock()) { mFirstBypassAttempt = false; animateKeyguardStatusBarIn(StackStateAnimator.ANIMATION_DURATION_STANDARD); } @@ -487,6 +489,7 @@ public class NotificationPanelViewController extends PanelViewController { StatusBarTouchableRegionManager statusBarTouchableRegionManager, ConversationNotificationManager conversationNotificationManager, MediaHierarchyManager mediaHierarchyManager, + BiometricUnlockController biometricUnlockController, StatusBarKeyguardViewManager statusBarKeyguardViewManager) { super(view, falsingManager, dozeLog, keyguardStateController, (SysuiStatusBarStateController) statusBarStateController, vibratorHelper, @@ -511,6 +514,7 @@ public class NotificationPanelViewController extends PanelViewController { mDisplayId = displayId; mPulseExpansionHandler = pulseExpansionHandler; mDozeParameters = dozeParameters; + mBiometricUnlockController = biometricUnlockController; pulseExpansionHandler.setPulseExpandAbortListener(() -> { if (mQs != null) { mQs.animateHeaderSlidingOut(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java index 06d35a36e3c4..5bb8fab8a62e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java @@ -688,8 +688,8 @@ public class PhoneStatusBarPolicy if (DEBUG) Log.d(TAG, "screenrecord: hiding icon during countdown"); mHandler.post(() -> mIconController.setIconVisibility(mSlotScreenRecord, false)); // Reset talkback priority - mIconController.setIconAccessibilityLiveRegion(mSlotScreenRecord, - View.ACCESSIBILITY_LIVE_REGION_NONE); + mHandler.post(() -> mIconController.setIconAccessibilityLiveRegion(mSlotScreenRecord, + View.ACCESSIBILITY_LIVE_REGION_NONE)); } @Override @@ -698,7 +698,7 @@ public class PhoneStatusBarPolicy mIconController.setIcon(mSlotScreenRecord, R.drawable.stat_sys_screen_record, mResources.getString(R.string.screenrecord_ongoing_screen_only)); - mIconController.setIconVisibility(mSlotScreenRecord, true); + mHandler.post(() -> mIconController.setIconVisibility(mSlotScreenRecord, true)); } @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java index e5024595d97e..5a6823879942 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java @@ -29,13 +29,12 @@ import android.testing.TestableLooper; import androidx.test.filters.SmallTest; -import com.android.internal.logging.UiEventLogger; import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; -import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.qs.QSTileHost; import com.android.systemui.screenrecord.RecordingController; +import com.android.systemui.statusbar.phone.KeyguardDismissUtil; import org.junit.Before; import org.junit.Test; @@ -44,18 +43,16 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; @RunWith(AndroidTestingRunner.class) -@TestableLooper.RunWithLooper +@TestableLooper.RunWithLooper(setAsMainLooper = true) @SmallTest public class ScreenRecordTileTest extends SysuiTestCase { @Mock private RecordingController mController; @Mock - private ActivityStarter mActivityStarter; - @Mock private QSTileHost mHost; @Mock - private UiEventLogger mUiEventLogger; + private KeyguardDismissUtil mKeyguardDismissUtil; private TestableLooper mTestableLooper; private ScreenRecordTile mTile; @@ -67,11 +64,10 @@ public class ScreenRecordTileTest extends SysuiTestCase { mTestableLooper = TestableLooper.get(this); mDependency.injectTestDependency(Dependency.BG_LOOPER, mTestableLooper.getLooper()); mController = mDependency.injectMockDependency(RecordingController.class); - mActivityStarter = mDependency.injectMockDependency(ActivityStarter.class); when(mHost.getContext()).thenReturn(mContext); - mTile = new ScreenRecordTile(mHost, mController, mActivityStarter, mUiEventLogger); + mTile = new ScreenRecordTile(mHost, mController, mKeyguardDismissUtil); } // Test that the tile is inactive and labeled correctly when the controller is neither starting @@ -89,6 +85,7 @@ public class ScreenRecordTileTest extends SysuiTestCase { mContext.getString(R.string.quick_settings_screen_record_start))); mTile.handleClick(); + mTestableLooper.processAllMessages(); verify(mController, times(1)).getPromptIntent(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java index b877c7fa6859..11ef3e33f9d0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java @@ -22,12 +22,14 @@ import static junit.framework.Assert.assertTrue; import static org.mockito.Mockito.verify; import android.app.PendingIntent; +import android.content.Intent; import android.os.Looper; import android.testing.AndroidTestingRunner; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; +import com.android.systemui.broadcast.BroadcastDispatcher; import org.junit.Before; import org.junit.Test; @@ -45,14 +47,18 @@ import org.mockito.MockitoAnnotations; public class RecordingControllerTest extends SysuiTestCase { @Mock - RecordingController.RecordingStateChangeCallback mCallback; + private RecordingController.RecordingStateChangeCallback mCallback; + @Mock + private BroadcastDispatcher mBroadcastDispatcher; + + private RecordingController mController; - RecordingController mController; + private static final int USER_ID = 10; @Before public void setUp() { MockitoAnnotations.initMocks(this); - mController = new RecordingController(mContext); + mController = new RecordingController(mBroadcastDispatcher); mController.addCallback(mCallback); } @@ -121,4 +127,27 @@ public class RecordingControllerTest extends SysuiTestCase { assertFalse(mController.isRecording()); verify(mCallback).onRecordingEnd(); } + + // Test that switching users will stop an ongoing recording + @Test + public void testUserChange() { + if (Looper.myLooper() == null) { + Looper.prepare(); + } + + // If we are recording + PendingIntent startIntent = Mockito.mock(PendingIntent.class); + PendingIntent stopIntent = Mockito.mock(PendingIntent.class); + mController.startCountdown(0, 0, startIntent, stopIntent); + mController.updateState(true); + + // and user is changed + Intent intent = new Intent(Intent.ACTION_USER_SWITCHED) + .putExtra(Intent.EXTRA_USER_HANDLE, USER_ID); + mController.mUserChangeReceiver.onReceive(mContext, intent); + + // Ensure that the recording was stopped + verify(mCallback).onRecordingEnd(); + assertFalse(mController.isRecording()); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java index 283a47ca3622..e98b6b69ee76 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java @@ -32,6 +32,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.logging.UiEventLogger; import com.android.systemui.SysuiTestCase; +import com.android.systemui.settings.CurrentUserContextTracker; import org.junit.Before; import org.junit.Test; @@ -58,6 +59,8 @@ public class RecordingServiceTest extends SysuiTestCase { private Notification mNotification; @Mock private Executor mExecutor; + @Mock + private CurrentUserContextTracker mUserContextTracker; private RecordingService mRecordingService; @@ -65,7 +68,7 @@ public class RecordingServiceTest extends SysuiTestCase { public void setUp() throws Exception { MockitoAnnotations.initMocks(this); mRecordingService = Mockito.spy(new RecordingService(mController, mExecutor, mUiEventLogger, - mNotificationManager)); + mNotificationManager, mUserContextTracker)); // Return actual context info doReturn(mContext).when(mRecordingService).getApplicationContext(); @@ -80,6 +83,8 @@ public class RecordingServiceTest extends SysuiTestCase { doNothing().when(mRecordingService).startForeground(anyInt(), any()); doReturn(mScreenMediaRecorder).when(mRecordingService).getRecorder(); + + doReturn(mContext).when(mUserContextTracker).getCurrentUserContext(); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java index c2d218140803..b0b66b87d421 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java @@ -180,6 +180,8 @@ public class NotificationPanelViewTest extends SysuiTestCase { @Mock private ConversationNotificationManager mConversationNotificationManager; @Mock + private BiometricUnlockController mBiometricUnlockController; + @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; private FlingAnimationUtils.Builder mFlingAnimationUtilsBuilder; @@ -238,7 +240,7 @@ public class NotificationPanelViewTest extends SysuiTestCase { mMetricsLogger, mActivityManager, mZenModeController, mConfigurationController, mFlingAnimationUtilsBuilder, mStatusBarTouchableRegionManager, mConversationNotificationManager, mMediaHiearchyManager, - mStatusBarKeyguardViewManager); + mBiometricUnlockController, mStatusBarKeyguardViewManager); mNotificationPanelViewController.initDependencies(mStatusBar, mGroupManager, mNotificationShelf, mNotificationAreaController, mScrimController); mNotificationPanelViewController.setHeadsUpManager(mHeadsUpManager); diff --git a/packages/Tethering/apex/manifest.json b/packages/Tethering/apex/manifest.json index 0c13491c5b6e..55ea563cb4b4 100644 --- a/packages/Tethering/apex/manifest.json +++ b/packages/Tethering/apex/manifest.json @@ -1,4 +1,4 @@ { "name": "com.android.tethering", - "version": 300802700 + "version": 300802800 } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index a5d99e019f98..e77b361c8c06 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -1667,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); @@ -14720,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. @@ -18521,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/SoundEffectsHelper.java b/services/core/java/com/android/server/audio/SoundEffectsHelper.java index cf5bc8d88c73..6d29b0e0078c 100644 --- a/services/core/java/com/android/server/audio/SoundEffectsHelper.java +++ b/services/core/java/com/android/server/audio/SoundEffectsHelper.java @@ -247,7 +247,7 @@ class SoundEffectsHelper { } Resource res = mResources.get(mEffects[effect]); - if (res.mSampleId != EFFECT_NOT_IN_SOUND_POOL && res.mLoaded) { + if (mSoundPool != null && res.mSampleId != EFFECT_NOT_IN_SOUND_POOL && res.mLoaded) { mSoundPool.play(res.mSampleId, volFloat, volFloat, 0, 0, 1.0f); } else { MediaPlayer mediaPlayer = new MediaPlayer(); @@ -511,7 +511,9 @@ class SoundEffectsHelper { } void onComplete(boolean success) { - mSoundPool.setOnLoadCompleteListener(null); + if (mSoundPool != null) { + mSoundPool.setOnLoadCompleteListener(null); + } for (OnEffectsLoadCompleteHandler handler : mLoadCompleteHandlers) { handler.run(success); } diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index cc9503995ad9..875bfdffafcd 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -1555,7 +1555,6 @@ class MediaRouter2ServiceImpl { long managerRequestId = (matchingRequest == null) ? MediaRoute2ProviderService.REQUEST_ID_NONE : matchingRequest.mManagerRequestId; - // Managers should know created session even if it's not requested. notifySessionCreatedToManagers(managerRequestId, sessionInfo); if (matchingRequest == null) { @@ -1575,18 +1574,6 @@ class MediaRouter2ServiceImpl { + "session=" + matchingRequest.mOldSession); } - String originalRouteId = matchingRequest.mRoute.getId(); - RouterRecord routerRecord = matchingRequest.mRouterRecord; - - if (!sessionInfo.getSelectedRoutes().contains(originalRouteId)) { - Slog.w(TAG, "Created session doesn't match the original request." - + " originalRouteId=" + originalRouteId - + ", uniqueRequestId=" + uniqueRequestId + ", sessionInfo=" + sessionInfo); - notifySessionCreationFailedToRouter(matchingRequest.mRouterRecord, - toOriginalRequestId(uniqueRequestId)); - return; - } - // Succeeded if (sessionInfo.isSystemSession() && !matchingRequest.mRouterRecord.mHasModifyAudioRoutingPermission) { @@ -1597,7 +1584,7 @@ class MediaRouter2ServiceImpl { notifySessionCreatedToRouter(matchingRequest.mRouterRecord, toOriginalRequestId(uniqueRequestId), sessionInfo); } - mSessionToRouterMap.put(sessionInfo.getId(), routerRecord); + mSessionToRouterMap.put(sessionInfo.getId(), matchingRequest.mRouterRecord); } private void onSessionInfoChangedOnHandler(@NonNull MediaRoute2Provider provider, 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/notification/ShortcutHelper.java b/services/core/java/com/android/server/notification/ShortcutHelper.java index ee02e3fa8140..94b690a4dfce 100644 --- a/services/core/java/com/android/server/notification/ShortcutHelper.java +++ b/services/core/java/com/android/server/notification/ShortcutHelper.java @@ -37,7 +37,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * Helper for querying shortcuts. @@ -249,8 +251,11 @@ public class ShortcutHelper { if (!TextUtils.isEmpty(shortcutId)) { packageBubbles.remove(shortcutId); } else { + // Copy the shortcut IDs to avoid a concurrent modification exception. + final Set<String> shortcutIds = new HashSet<>(packageBubbles.keySet()); + // Check if there was a matching entry - for (String pkgShortcutId : packageBubbles.keySet()) { + for (String pkgShortcutId : shortcutIds) { String entryKey = packageBubbles.get(pkgShortcutId); if (r.getKey().equals(entryKey)) { // No longer has shortcut id so remove it diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index a9d3a07b4e68..088c5daf30a4 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -11733,6 +11733,8 @@ public class PackageManagerService extends IPackageManager.Stub } } else { parsedPackage + // Non system apps cannot mark any broadcast as protected + .clearProtectedBroadcasts() // non system apps can't be flagged as core .setCoreApp(false) // clear flags not applicable to regular apps @@ -11744,7 +11746,6 @@ public class PackageManagerService extends IPackageManager.Stub } if ((scanFlags & SCAN_AS_PRIVILEGED) == 0) { parsedPackage - .clearProtectedBroadcasts() .markNotActivitiesAsNotExportedIfSingleUser(); } diff --git a/services/core/java/com/android/server/pm/TEST_MAPPING b/services/core/java/com/android/server/pm/TEST_MAPPING index eb51cc3cd25c..4b83d70ea34e 100644 --- a/services/core/java/com/android/server/pm/TEST_MAPPING +++ b/services/core/java/com/android/server/pm/TEST_MAPPING @@ -71,7 +71,12 @@ ] }, { - "name": "PackageManagerServiceHostTests" + "name": "PackageManagerServiceHostTests", + "options": [ + { + "include-annotation": "android.platform.test.annotations.Presubmit" + } + ] } ], "postsubmit": [ @@ -87,6 +92,9 @@ "name": "CtsAppSecurityHostTestCases" }, { + "name": "PackageManagerServiceHostTests" + }, + { "name": "FrameworksServicesTests", "options": [ { diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java index b9240c78d711..9bc702d4c094 100644 --- a/services/core/java/com/android/server/wm/AccessibilityController.java +++ b/services/core/java/com/android/server/wm/AccessibilityController.java @@ -855,8 +855,7 @@ final class AccessibilityController { mTempLayer = 0; mDisplayContent.forAllWindows((w) -> { if (w.isOnScreen() && w.isVisibleLw() - && (w.mAttrs.alpha != 0) - && !w.mWinAnimator.mEnterAnimationPending) { + && (w.mAttrs.alpha != 0)) { mTempLayer++; outWindows.put(mTempLayer, w); } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 319fd18faca5..2a68a194b369 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -3556,10 +3556,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A WindowState getImeTargetBelowWindow(WindowState w) { final int index = mChildren.indexOf(w); if (index > 0) { - final WindowState target = mChildren.get(index - 1); - if (target.canBeImeTarget()) { - return target; - } + return mChildren.get(index - 1) + .getWindow(WindowState::canBeImeTarget); } return null; } diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 25842f58579d..31712ef85add 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -1668,6 +1668,12 @@ class ActivityStarter { mTargetStack.setAlwaysOnTop(true); } } + if (!mTargetStack.isTopStackInDisplayArea() && mService.mInternal.isDreaming()) { + // Launching underneath dream activity (fullscreen, always-on-top). Run the launch- + // -behind transition so the Activity gets created and starts in visible state. + mLaunchTaskBehind = true; + r.mLaunchTaskBehind = true; + } } mService.mUgmInternal.grantUriPermissionUncheckedFromIntent(intentGrants, @@ -1917,6 +1923,12 @@ class ActivityStarter { return START_SUCCESS; } + // At this point we are certain we want the task moved to the front. If we need to dismiss + // any other always-on-top stacks, now is the time to do it. + if (targetTaskTop.canTurnScreenOn() && mService.mInternal.isDreaming()) { + targetTaskTop.mStackSupervisor.wakeUp("recycleTask#turnScreenOnFlag"); + } + if (mMovedToFront) { // We moved the task to front, use starting window to hide initial drawn delay. targetTaskTop.showStartingWindow(null /* prev */, false /* newTask */, 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/InputMonitor.java b/services/core/java/com/android/server/wm/InputMonitor.java index 0e2611d1ddb9..8734b5efa45d 100644 --- a/services/core/java/com/android/server/wm/InputMonitor.java +++ b/services/core/java/com/android/server/wm/InputMonitor.java @@ -477,11 +477,12 @@ final class InputMonitor { mService.getRecentsAnimationController(); final boolean shouldApplyRecentsInputConsumer = recentsAnimationController != null && recentsAnimationController.shouldApplyInputConsumer(w.mActivityRecord); - if (inputWindowHandle == null || w.mRemoved) { + if (inputChannel == null || inputWindowHandle == null || w.mRemoved + || (w.cantReceiveTouchInput() && !shouldApplyRecentsInputConsumer)) { if (w.mWinAnimator.hasSurface()) { mInputTransaction.setInputWindowInfo( - w.mWinAnimator.mSurfaceController.getClientViewRootSurface(), - mInvalidInputWindow); + w.mWinAnimator.mSurfaceController.getClientViewRootSurface(), + mInvalidInputWindow); } // Skip this window because it cannot possibly receive input. return; @@ -490,23 +491,9 @@ final class InputMonitor { final int flags = w.mAttrs.flags; final int privateFlags = w.mAttrs.privateFlags; final int type = w.mAttrs.type; + final boolean hasFocus = w.isFocused(); final boolean isVisible = w.isVisibleLw(); - // Assign an InputInfo with type to the overlay window which can't receive input event. - // This is used to omit Surfaces from occlusion detection. - if (inputChannel == null - || (w.cantReceiveTouchInput() && !shouldApplyRecentsInputConsumer)) { - if (!w.mWinAnimator.hasSurface()) { - return; - } - populateOverlayInputInfo(inputWindowHandle, w.getName(), type, isVisible); - mInputTransaction.setInputWindowInfo( - w.mWinAnimator.mSurfaceController.getClientViewRootSurface(), - inputWindowHandle); - return; - } - - final boolean hasFocus = w.isFocused(); if (mAddRecentsAnimationInputConsumerHandle && shouldApplyRecentsInputConsumer) { if (recentsAnimationController.updateInputConsumerForApp( mRecentsAnimationInputConsumer.mWindowHandle, hasFocus)) { @@ -568,22 +555,6 @@ final class InputMonitor { } } - private static void populateOverlayInputInfo(final InputWindowHandle inputWindowHandle, - final String name, final int type, final boolean isVisible) { - inputWindowHandle.name = name; - inputWindowHandle.layoutParamsType = type; - inputWindowHandle.dispatchingTimeoutNanos = - WindowManagerService.DEFAULT_INPUT_DISPATCHING_TIMEOUT_NANOS; - inputWindowHandle.visible = isVisible; - inputWindowHandle.canReceiveKeys = false; - inputWindowHandle.hasFocus = false; - inputWindowHandle.ownerPid = myPid(); - inputWindowHandle.ownerUid = myUid(); - inputWindowHandle.inputFeatures = INPUT_FEATURE_NO_INPUT_CHANNEL; - inputWindowHandle.scaleFactor = 1; - inputWindowHandle.layoutParamsFlags = FLAG_NOT_TOUCH_MODAL; - } - /** * Helper function to generate an InputInfo with type SECURE_SYSTEM_OVERLAY. This input * info will not have an input channel or be touchable, but is used to omit Surfaces @@ -593,7 +564,16 @@ final class InputMonitor { static void setTrustedOverlayInputInfo(SurfaceControl sc, SurfaceControl.Transaction t, int displayId, String name) { InputWindowHandle inputWindowHandle = new InputWindowHandle(null, displayId); - populateOverlayInputInfo(inputWindowHandle, name, TYPE_SECURE_SYSTEM_OVERLAY, true); + inputWindowHandle.name = name; + inputWindowHandle.layoutParamsType = TYPE_SECURE_SYSTEM_OVERLAY; + inputWindowHandle.dispatchingTimeoutNanos = -1; + inputWindowHandle.visible = true; + inputWindowHandle.canReceiveKeys = false; + inputWindowHandle.hasFocus = false; + inputWindowHandle.ownerPid = myPid(); + inputWindowHandle.ownerUid = myUid(); + inputWindowHandle.inputFeatures = INPUT_FEATURE_NO_INPUT_CHANNEL; + inputWindowHandle.scaleFactor = 1; t.setInputWindowInfo(sc, inputWindowHandle); } } diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java index 7b690383f5f9..e23080724000 100644 --- a/services/core/java/com/android/server/wm/TaskDisplayArea.java +++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java @@ -564,16 +564,23 @@ final class TaskDisplayArea extends DisplayArea<ActivityStack> { // Apps and their containers are not allowed to specify an orientation while using // root tasks...except for the home stack if it is not resizable and currently // visible (top of) its root task. - if (mRootHomeTask != null && mRootHomeTask.isVisible() - && !mRootHomeTask.isResizeable()) { + if (mRootHomeTask != null && !mRootHomeTask.isResizeable()) { // Manually nest one-level because because getOrientation() checks fillsParent() // which checks that requestedOverrideBounds() is empty. However, in this case, // it is not empty because it's been overridden to maintain the fullscreen size // within a smaller split-root. final Task topHomeTask = mRootHomeTask.getTopMostTask(); - final int orientation = topHomeTask.getOrientation(); - if (orientation != SCREEN_ORIENTATION_UNSET) { - return orientation; + final ActivityRecord topHomeActivity = topHomeTask.getTopNonFinishingActivity(); + // If a home activity is in the process of launching and isn't yet visible we + // should still respect the stack's preferred orientation to ensure rotation occurs + // before the home activity finishes launching. + final boolean isHomeActivityLaunching = topHomeActivity != null + && topHomeActivity.mVisibleRequested; + if (topHomeTask.isVisible() || isHomeActivityLaunching) { + final int orientation = topHomeTask.getOrientation(); + if (orientation != SCREEN_ORIENTATION_UNSET) { + return orientation; + } } } return SCREEN_ORIENTATION_UNSPECIFIED; diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 49ef4e46bfeb..708abe28c806 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -2385,23 +2385,23 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP return false; } - if (PixelFormat.formatHasAlpha(mAttrs.format)) { + if (mAttrs.type == TYPE_APPLICATION_STARTING) { + // Ignore mayUseInputMethod for starting window for now. + // TODO(b/159911356): Remove this special casing (originally added in commit e75d872). + } else if (PixelFormat.formatHasAlpha(mAttrs.format)) { // Support legacy use cases where transparent windows can still be ime target with // FLAG_NOT_FOCUSABLE and ALT_FOCUSABLE_IM set. // Certain apps listen for IME insets using transparent windows and ADJUST_NOTHING to // manually synchronize app content to IME animation b/144619551. // TODO(b/145812508): remove this once new focus management is complete b/141738570 final int fl = mAttrs.flags & (FLAG_NOT_FOCUSABLE | FLAG_ALT_FOCUSABLE_IM); - final int type = mAttrs.type; // Can only be an IME target if both FLAG_NOT_FOCUSABLE and FLAG_ALT_FOCUSABLE_IM are // set or both are cleared...and not a starting window. - if (fl != 0 && fl != (FLAG_NOT_FOCUSABLE | FLAG_ALT_FOCUSABLE_IM) - && type != TYPE_APPLICATION_STARTING) { + if (fl != 0 && fl != (FLAG_NOT_FOCUSABLE | FLAG_ALT_FOCUSABLE_IM)) { return false; } - } else if (!WindowManager.LayoutParams.mayUseInputMethod(mAttrs.flags) - || mAttrs.type == TYPE_APPLICATION_STARTING) { + } else if (!WindowManager.LayoutParams.mayUseInputMethod(mAttrs.flags)) { // Can be an IME target only if: // 1. FLAG_NOT_FOCUSABLE is not set // 2. FLAG_ALT_FOCUSABLE_IM is not set @@ -2696,7 +2696,9 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP Settings.Global.THEATER_MODE_ON, 0) == 0; boolean canTurnScreenOn = mActivityRecord == null || mActivityRecord.currentLaunchCanTurnScreenOn(); - if (allowTheaterMode && canTurnScreenOn && !mPowerManagerWrapper.isInteractive()) { + if (allowTheaterMode && canTurnScreenOn + && (mWmService.mAtmInternal.isDreaming() + || !mPowerManagerWrapper.isInteractive())) { if (DEBUG_VISIBILITY || DEBUG_POWER) { Slog.v(TAG, "Relayout window turning screen on: " + this); } diff --git a/services/tests/PackageManagerServiceTests/OWNERS b/services/tests/PackageManagerServiceTests/OWNERS new file mode 100644 index 000000000000..182dfe8fca9e --- /dev/null +++ b/services/tests/PackageManagerServiceTests/OWNERS @@ -0,0 +1,3 @@ +chiuwinson@google.com +patb@google.com +toddke@google.com diff --git a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/HostUtils.kt b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/HostUtils.kt index 490f96d8f426..234fcf19062d 100644 --- a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/HostUtils.kt +++ b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/HostUtils.kt @@ -22,10 +22,16 @@ import java.io.File import java.io.FileOutputStream internal fun SystemPreparer.pushApk(file: String, partition: Partition) = - pushResourceFile(file, HostUtils.makePathForApk(file, partition)) - -internal fun SystemPreparer.deleteApk(file: String, partition: Partition) = - deleteFile(partition.baseFolder.resolve(file.removeSuffix(".apk")).toString()) + pushResourceFile(file, HostUtils.makePathForApk(file, partition).toString()) + +internal fun SystemPreparer.deleteApkFolders( + partition: Partition, + vararg javaResourceNames: String +) = apply { + javaResourceNames.forEach { + deleteFile(partition.baseAppFolder.resolve(it.removeSuffix(".apk")).toString()) + } +} internal object HostUtils { @@ -40,10 +46,9 @@ internal object HostUtils { makePathForApk(File(fileName), partition) fun makePathForApk(file: File, partition: Partition) = - partition.baseFolder + partition.baseAppFolder .resolve(file.nameWithoutExtension) .resolve(file.name) - .toString() fun copyResourceToHostFile(javaResourceName: String, file: File): File { javaClass.classLoader!!.getResource(javaResourceName).openStream().use { input -> diff --git a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/InvalidNewSystemAppTest.kt b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/InvalidNewSystemAppTest.kt index 98e045d0a203..39b40d8d3195 100644 --- a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/InvalidNewSystemAppTest.kt +++ b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/InvalidNewSystemAppTest.kt @@ -45,23 +45,24 @@ class InvalidNewSystemAppTest : BaseHostJUnit4Test() { private val tempFolder = TemporaryFolder() private val preparer: SystemPreparer = SystemPreparer(tempFolder, - SystemPreparer.RebootStrategy.START_STOP, deviceRebootRule) { this.device } + SystemPreparer.RebootStrategy.FULL, deviceRebootRule) { this.device } @get:Rule val rules = RuleChain.outerRule(tempFolder).around(preparer)!! + private val filePath = HostUtils.makePathForApk("PackageManagerDummyApp.apk", Partition.PRODUCT) @Before @After - fun uninstallDataPackage() { + fun removeApk() { device.uninstallPackage(TEST_PKG_NAME) + device.deleteFile(filePath.parent.toString()) + device.reboot() } @Test fun verify() { // First, push a system app to the device and then update it so there's a data variant - val filePath = HostUtils.makePathForApk("PackageManagerDummyApp.apk", Partition.PRODUCT) - - preparer.pushResourceFile(VERSION_ONE, filePath) + preparer.pushResourceFile(VERSION_ONE, filePath.toString()) .reboot() val versionTwoFile = HostUtils.copyResourceToHostFile(VERSION_TWO, tempFolder.newFile()) @@ -69,8 +70,8 @@ class InvalidNewSystemAppTest : BaseHostJUnit4Test() { assertThat(device.installPackage(versionTwoFile, true)).isNull() // Then push a bad update to the system, overwriting the existing file as if an OTA occurred - preparer.deleteFile(filePath) - .pushResourceFile(VERSION_THREE_INVALID, filePath) + preparer.deleteFile(filePath.toString()) + .pushResourceFile(VERSION_THREE_INVALID, filePath.toString()) .reboot() // This will remove the package from the device, which is expected diff --git a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/OriginalPackageMigrationTest.kt b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/OriginalPackageMigrationTest.kt index 90494c591363..fb0348c3146b 100644 --- a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/OriginalPackageMigrationTest.kt +++ b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/OriginalPackageMigrationTest.kt @@ -20,6 +20,8 @@ import com.android.internal.util.test.SystemPreparer import com.android.tradefed.testtype.DeviceJUnit4ClassRunner import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before import org.junit.ClassRule import org.junit.Rule import org.junit.Test @@ -43,11 +45,18 @@ class OriginalPackageMigrationTest : BaseHostJUnit4Test() { private val tempFolder = TemporaryFolder() private val preparer: SystemPreparer = SystemPreparer(tempFolder, - SystemPreparer.RebootStrategy.START_STOP, deviceRebootRule) { this.device } + SystemPreparer.RebootStrategy.FULL, deviceRebootRule) { this.device } @get:Rule val rules = RuleChain.outerRule(tempFolder).around(preparer)!! + @Before + @After + fun deleteApkFolders() { + preparer.deleteApkFolders(Partition.SYSTEM, VERSION_ONE, VERSION_TWO, VERSION_THREE, + NEW_PKG) + } + @Test fun lowerVersion() { runForApk(VERSION_ONE) @@ -71,28 +80,28 @@ class OriginalPackageMigrationTest : BaseHostJUnit4Test() { preparer.pushApk(apk, Partition.SYSTEM) .reboot() - device.getAppPackageInfo(TEST_PKG_NAME).run { - assertThat(codePath).contains(apk.removeSuffix(".apk")) - } + assertCodePath(apk) // Ensure data is preserved by writing to the original dataDir val file = tempFolder.newFile().apply { writeText("Test") } device.pushFile(file, "${HostUtils.getDataDir(device, TEST_PKG_NAME)}/files/test.txt") - preparer.deleteApk(apk, Partition.SYSTEM) + preparer.deleteApkFolders(Partition.SYSTEM, apk) .pushApk(NEW_PKG, Partition.SYSTEM) .reboot() - device.getAppPackageInfo(TEST_PKG_NAME) - .run { - assertThat(this.toString()).isNotEmpty() - assertThat(codePath) - .contains(NEW_PKG.removeSuffix(".apk")) - } + assertCodePath(NEW_PKG) // And then reading the data contents back assertThat(device.pullFileContents( "${HostUtils.getDataDir(device, TEST_PKG_NAME)}/files/test.txt")) .isEqualTo("Test") } + + private fun assertCodePath(apk: String) { + // dumpsys package and therefore device.getAppPackageInfo doesn't work here for some reason, + // so parse the package dump directly to see if the path matches. + assertThat(device.executeShellCommand("pm dump $TEST_PKG_NAME")) + .contains(HostUtils.makePathForApk(apk, Partition.SYSTEM).parent.toString()) + } } diff --git a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/Partition.kt b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/Partition.kt index 35192a73ceda..654c11c5bf81 100644 --- a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/Partition.kt +++ b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/Partition.kt @@ -20,7 +20,7 @@ import java.nio.file.Path import java.nio.file.Paths // Unfortunately no easy way to access PMS SystemPartitions, so mock them here -internal enum class Partition(val baseFolder: Path) { +internal enum class Partition(val baseAppFolder: Path) { SYSTEM("/system/app"), VENDOR("/vendor/app"), PRODUCT("/product/app"), diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java index cdafd32cbbb5..fde40aa77a0e 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java @@ -63,7 +63,6 @@ import static com.android.server.am.ProcessList.VISIBLE_APP_ADJ; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.AdditionalAnswers.answer; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.anyLong; @@ -171,7 +170,6 @@ public class MockingOomAdjusterTests { mock(OomAdjProfiler.class)); doReturn(new ActivityManagerService.ProcessChangeItem()).when(sService) .enqueueProcessChangeItemLocked(anyInt(), anyInt()); - doReturn(true).when(sService).containsTopUiOrRunningRemoteAnimOrEmptyLocked(any()); sService.mOomAdjuster = new OomAdjuster(sService, sService.mProcessList, mock(ActiveUids.class)); sService.mOomAdjuster.mAdjSeq = 10000; @@ -268,21 +266,6 @@ public class MockingOomAdjusterTests { @SuppressWarnings("GuardedBy") @Test - public void testUpdateOomAdj_DoOne_TopApp_PreemptedByTopUi() { - ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID, - MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true)); - doReturn(PROCESS_STATE_TOP).when(sService.mAtmInternal).getTopProcessState(); - doReturn(app).when(sService).getTopAppLocked(); - doReturn(false).when(sService).containsTopUiOrRunningRemoteAnimOrEmptyLocked(eq(app)); - sService.mWakefulness = PowerManagerInternal.WAKEFULNESS_AWAKE; - sService.mOomAdjuster.updateOomAdjLocked(app, false, OomAdjuster.OOM_ADJ_REASON_NONE); - doReturn(null).when(sService).getTopAppLocked(); - - assertProcStates(app, PROCESS_STATE_TOP, FOREGROUND_APP_ADJ, SCHED_GROUP_DEFAULT); - } - - @SuppressWarnings("GuardedBy") - @Test public void testUpdateOomAdj_DoOne_RunningInstrumentation() { ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID, MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true)); diff --git a/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java index 4e2f9a495fe8..924ad7f3f5a1 100644 --- a/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java @@ -377,11 +377,11 @@ public class BlobStoreManagerServiceTest { } private BlobMetadata createBlobMetadataMock(long blobId, File blobFile, - BlobHandle blobHandle, boolean hasLeases) { + BlobHandle blobHandle, boolean hasValidLeases) { final BlobMetadata blobMetadata = mock(BlobMetadata.class); doReturn(blobId).when(blobMetadata).getBlobId(); doReturn(blobFile).when(blobMetadata).getBlobFile(); - doReturn(hasLeases).when(blobMetadata).hasLeases(); + doReturn(hasValidLeases).when(blobMetadata).hasValidLeases(); doReturn(blobHandle).when(blobMetadata).getBlobHandle(); doCallRealMethod().when(blobMetadata).shouldBeDeleted(anyBoolean()); doReturn(true).when(blobMetadata).hasLeaseWaitTimeElapsedForAll(); diff --git a/services/tests/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/uiservicestests/src/com/android/server/notification/ShortcutHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ShortcutHelperTest.java index c700a090fa2e..eca71b69ec0b 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ShortcutHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ShortcutHelperTest.java @@ -45,6 +45,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.util.ArrayList; @@ -85,14 +86,19 @@ public class ShortcutHelperTest extends UiServiceTestCase { mShortcutHelper = new ShortcutHelper( mLauncherApps, mShortcutListener, mShortcutServiceInternal); - when(mNr.getKey()).thenReturn(KEY); - when(mNr.getSbn()).thenReturn(mSbn); when(mSbn.getPackageName()).thenReturn(PKG); - when(mNr.getNotification()).thenReturn(mNotif); - when(mNr.getShortcutInfo()).thenReturn(mShortcutInfo); when(mShortcutInfo.getId()).thenReturn(SHORTCUT_ID); when(mNotif.getBubbleMetadata()).thenReturn(mBubbleMetadata); when(mBubbleMetadata.getShortcutId()).thenReturn(SHORTCUT_ID); + + setUpMockNotificationRecord(mNr, KEY); + } + + private void setUpMockNotificationRecord(NotificationRecord mockRecord, String key) { + when(mockRecord.getKey()).thenReturn(key); + when(mockRecord.getSbn()).thenReturn(mSbn); + when(mockRecord.getNotification()).thenReturn(mNotif); + when(mockRecord.getShortcutInfo()).thenReturn(mShortcutInfo); } private LauncherApps.Callback addShortcutBubbleAndVerifyListener() { @@ -159,9 +165,31 @@ public class ShortcutHelperTest extends UiServiceTestCase { // First set it up to listen addShortcutBubbleAndVerifyListener(); - // Clear out shortcutId - when(mNr.getShortcutInfo()).thenReturn(null); - mShortcutHelper.maybeListenForShortcutChangesForBubbles(mNr, + NotificationRecord validMock1 = Mockito.mock(NotificationRecord.class); + setUpMockNotificationRecord(validMock1, "KEY1"); + + NotificationRecord validMock2 = Mockito.mock(NotificationRecord.class); + setUpMockNotificationRecord(validMock2, "KEY2"); + + NotificationRecord validMock3 = Mockito.mock(NotificationRecord.class); + setUpMockNotificationRecord(validMock3, "KEY3"); + + mShortcutHelper.maybeListenForShortcutChangesForBubbles(validMock1, + false /* removed */, + null /* handler */); + + mShortcutHelper.maybeListenForShortcutChangesForBubbles(validMock2, + false /* removed */, + null /* handler */); + + mShortcutHelper.maybeListenForShortcutChangesForBubbles(validMock3, + false /* removed */, + null /* handler */); + + // Clear out shortcutId of the bubble in the middle, to double check that we don't hit a + // concurrent modification exception (removing the last bubble would sidestep that check). + when(validMock2.getShortcutInfo()).thenReturn(null); + mShortcutHelper.maybeListenForShortcutChangesForBubbles(validMock2, false /* removed */, null /* handler */); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index 105af4fce394..30af1d34f558 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -43,6 +43,7 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMA import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE; import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; @@ -273,6 +274,38 @@ public class DisplayContentTests extends WindowTestsBase { assertEquals(childWin, imeTarget); } + @Test + public void testComputeImeTarget_startingWindow() { + ActivityRecord activity = createActivityRecord(mDisplayContent, + WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD); + + final WindowState startingWin = createWindow(null, TYPE_APPLICATION_STARTING, activity, + "startingWin"); + startingWin.setHasSurface(true); + assertTrue(startingWin.canBeImeTarget()); + + WindowState imeTarget = mDisplayContent.computeImeTarget(false /* updateImeTarget */); + assertEquals(startingWin, imeTarget); + startingWin.mHidden = false; + + // Verify that an app window launching behind the starting window becomes the target + final WindowState appWin = createWindow(null, TYPE_BASE_APPLICATION, activity, "appWin"); + appWin.setHasSurface(true); + assertTrue(appWin.canBeImeTarget()); + + imeTarget = mDisplayContent.computeImeTarget(false /* updateImeTarget */); + assertEquals(appWin, imeTarget); + appWin.mHidden = false; + + // Verify that an child window can be an ime target even behind a launching app window + final WindowState childWin = createWindow(appWin, + TYPE_APPLICATION_ATTACHED_DIALOG, "childWin"); + childWin.setHasSurface(true); + assertTrue(childWin.canBeImeTarget()); + imeTarget = mDisplayContent.computeImeTarget(false /* updateImeTarget */); + assertEquals(childWin, imeTarget); + } + /** * This tests stack movement between displays and proper stack's, task's and app token's display * container references updates. diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java index 786f8d8af024..8c3661b409f4 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java @@ -28,14 +28,17 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.android.server.wm.ActivityStack.ActivityState.RESUMED; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -207,6 +210,40 @@ public class TaskDisplayAreaTests extends WindowTestsBase { false /* reuseCandidate */); } + @Test + public void testGetOrientation_nonResizableHomeStackWithHomeActivityPendingVisibilityChange() { + final RootWindowContainer rootWindowContainer = mWm.mAtmService.mRootWindowContainer; + final TaskDisplayArea defaultTaskDisplayArea = + rootWindowContainer.getDefaultTaskDisplayArea(); + + final ActivityStack rootHomeTask = defaultTaskDisplayArea.getRootHomeTask(); + rootHomeTask.mResizeMode = RESIZE_MODE_UNRESIZEABLE; + + final ActivityStack primarySplitTask = + new ActivityTestsBase.StackBuilder(rootWindowContainer) + .setTaskDisplayArea(defaultTaskDisplayArea) + .setWindowingMode(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY) + .setActivityType(ACTIVITY_TYPE_STANDARD) + .setOnTop(true) + .setCreateActivity(true) + .build(); + ActivityRecord primarySplitActivity = primarySplitTask.getTopNonFinishingActivity(); + assertNotNull(primarySplitActivity); + primarySplitActivity.setState(RESUMED, + "testGetOrientation_nonResizableHomeStackWithHomeActivityPendingVisibilityChange"); + + ActivityRecord homeActivity = rootHomeTask.getTopNonFinishingActivity(); + if (homeActivity == null) { + homeActivity = new ActivityTestsBase.ActivityBuilder(mWm.mAtmService) + .setStack(rootHomeTask).setCreateTask(true).build(); + } + homeActivity.setVisible(false); + homeActivity.mVisibleRequested = true; + assertFalse(rootHomeTask.isVisible()); + + assertEquals(rootWindowContainer.getOrientation(), rootHomeTask.getOrientation()); + } + private void assertGetOrCreateStack(int windowingMode, int activityType, Task candidateTask, boolean reuseCandidate) { final TaskDisplayArea taskDisplayArea = candidateTask.getDisplayArea(); diff --git a/tests/utils/hostutils/src/com/android/internal/util/test/SystemPreparer.java b/tests/utils/hostutils/src/com/android/internal/util/test/SystemPreparer.java index 6bd6985f9675..f30c35aca8da 100644 --- a/tests/utils/hostutils/src/com/android/internal/util/test/SystemPreparer.java +++ b/tests/utils/hostutils/src/com/android/internal/util/test/SystemPreparer.java @@ -356,6 +356,9 @@ public class SystemPreparer extends ExternalResource { /** * Uses shell stop && start to "reboot" the device. May leave invalid state after each test. * Whether this matters or not depends on what's being tested. + * + * TODO(b/159540015): There's a bug with this causing unnecessary disk space usage, which + * can eventually lead to an insufficient storage space error. */ START_STOP } |