/* * 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.internal.app; import static android.graphics.PixelFormat.TRANSLUCENT; import android.animation.ObjectAnimator; import android.app.ActionBar; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.provider.Settings; import android.util.DisplayMetrics; import android.util.Log; import android.view.Gravity; import android.view.HapticFeedbackConstants; import android.view.MotionEvent; import android.view.View; import android.view.animation.DecelerateInterpolator; import android.view.animation.OvershootInterpolator; import android.widget.AnalogClock; import android.widget.FrameLayout; import android.widget.ImageView; import com.android.internal.R; import org.json.JSONObject; import java.time.Clock; import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; /** * @hide */ public class PlatLogoActivity extends Activity { private static final String TAG = "PlatLogoActivity"; private static final String S_EGG_UNLOCK_SETTING = "egg_mode_s"; private SettableAnalogClock mClock; private ImageView mLogo; private BubblesDrawable mBg; @Override protected void onPause() { super.onPause(); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getWindow().setNavigationBarColor(0); getWindow().setStatusBarColor(0); final ActionBar ab = getActionBar(); if (ab != null) ab.hide(); final FrameLayout layout = new FrameLayout(this); mClock = new SettableAnalogClock(this); final DisplayMetrics dm = getResources().getDisplayMetrics(); final float dp = dm.density; final int minSide = Math.min(dm.widthPixels, dm.heightPixels); final int widgetSize = (int) (minSide * 0.75); final FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(widgetSize, widgetSize); lp.gravity = Gravity.CENTER; layout.addView(mClock, lp); mLogo = new ImageView(this); mLogo.setVisibility(View.GONE); mLogo.setImageResource(R.drawable.platlogo); layout.addView(mLogo, lp); mBg = new BubblesDrawable(); mBg.setLevel(0); mBg.avoid = widgetSize / 2; mBg.padding = 0.5f * dp; mBg.minR = 1 * dp; layout.setBackground(mBg); layout.setOnLongClickListener(mBg); setContentView(layout); } private boolean shouldWriteSettings() { return getPackageName().equals("android"); } private void launchNextStage(boolean locked) { mClock.animate() .alpha(0f).scaleX(0.5f).scaleY(0.5f) .withEndAction(() -> mClock.setVisibility(View.GONE)) .start(); mLogo.setAlpha(0f); mLogo.setScaleX(0.5f); mLogo.setScaleY(0.5f); mLogo.setVisibility(View.VISIBLE); mLogo.animate() .alpha(1f) .scaleX(1f) .scaleY(1f) .setInterpolator(new OvershootInterpolator()) .start(); mLogo.postDelayed(() -> { final ObjectAnimator anim = ObjectAnimator.ofInt(mBg, "level", 0, 10000); anim.setInterpolator(new DecelerateInterpolator(1f)); anim.start(); }, 500 ); final ContentResolver cr = getContentResolver(); try { if (shouldWriteSettings()) { Log.v(TAG, "Saving egg unlock=" + locked); syncTouchPressure(); Settings.System.putLong(cr, S_EGG_UNLOCK_SETTING, locked ? 0 : System.currentTimeMillis()); } } catch (RuntimeException e) { Log.e(TAG, "Can't write settings", e); } try { startActivity(new Intent(Intent.ACTION_MAIN) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK) .addCategory("com.android.internal.category.PLATLOGO")); } catch (ActivityNotFoundException ex) { Log.e("com.android.internal.app.PlatLogoActivity", "No more eggs."); } //finish(); // no longer finish upon unlock; it's fun to frob the dial } static final String TOUCH_STATS = "touch.stats"; double mPressureMin = 0, mPressureMax = -1; private void measureTouchPressure(MotionEvent event) { final float pressure = event.getPressure(); switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: if (mPressureMax < 0) { mPressureMin = mPressureMax = pressure; } break; case MotionEvent.ACTION_MOVE: if (pressure < mPressureMin) mPressureMin = pressure; if (pressure > mPressureMax) mPressureMax = pressure; break; } } private void syncTouchPressure() { try { final String touchDataJson = Settings.System.getString( getContentResolver(), TOUCH_STATS); final JSONObject touchData = new JSONObject( touchDataJson != null ? touchDataJson : "{}"); if (touchData.has("min")) { mPressureMin = Math.min(mPressureMin, touchData.getDouble("min")); } if (touchData.has("max")) { mPressureMax = Math.max(mPressureMax, touchData.getDouble("max")); } if (mPressureMax >= 0) { touchData.put("min", mPressureMin); touchData.put("max", mPressureMax); if (shouldWriteSettings()) { Settings.System.putString(getContentResolver(), TOUCH_STATS, touchData.toString()); } } } catch (Exception e) { Log.e("com.android.internal.app.PlatLogoActivity", "Can't write touch settings", e); } } @Override public void onStart() { super.onStart(); syncTouchPressure(); } @Override public void onStop() { syncTouchPressure(); super.onStop(); } /** * Subclass of AnalogClock that allows the user to flip up the glass and adjust the hands. */ public class SettableAnalogClock extends AnalogClock { private int mOverrideHour = -1; private int mOverrideMinute = -1; private boolean mOverride = false; public SettableAnalogClock(Context context) { super(context); } @Override protected Instant now() { final Instant realNow = super.now(); final ZoneId tz = Clock.systemDefaultZone().getZone(); final ZonedDateTime zdTime = realNow.atZone(tz); if (mOverride) { if (mOverrideHour < 0) { mOverrideHour = zdTime.getHour(); } return Clock.fixed(zdTime .withHour(mOverrideHour) .withMinute(mOverrideMinute) .withSecond(0) .toInstant(), tz).instant(); } else { return realNow; } } double toPositiveDegrees(double rad) { return (Math.toDegrees(rad) + 360 - 90) % 360; } @Override public boolean onTouchEvent(MotionEvent ev) { switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: mOverride = true; // pass through case MotionEvent.ACTION_MOVE: measureTouchPressure(ev); float x = ev.getX(); float y = ev.getY(); float cx = getWidth() / 2f; float cy = getHeight() / 2f; float angle = (float) toPositiveDegrees(Math.atan2(x - cx, y - cy)); int minutes = (75 - (int) (angle / 6)) % 60; int minuteDelta = minutes - mOverrideMinute; if (minuteDelta != 0) { if (Math.abs(minuteDelta) > 45 && mOverrideHour >= 0) { int hourDelta = (minuteDelta < 0) ? 1 : -1; mOverrideHour = (mOverrideHour + 24 + hourDelta) % 24; } mOverrideMinute = minutes; if (mOverrideMinute == 0) { performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); if (getScaleX() == 1f) { setScaleX(1.05f); setScaleY(1.05f); animate().scaleX(1f).scaleY(1f).setDuration(150).start(); } } else { performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK); } onTimeChanged(); postInvalidate(); } return true; case MotionEvent.ACTION_UP: if (mOverrideMinute == 0 && (mOverrideHour % 12) == 1) { Log.v(TAG, "13:00"); performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); launchNextStage(false); } return true; } return false; } } private static final String[][] EMOJI_SETS = { {"๐Ÿ‡", "๐Ÿˆ", "๐Ÿ‰", "๐ŸŠ", "๐Ÿ‹", "๐ŸŒ", "๐Ÿ", "๐Ÿฅญ", "๐ŸŽ", "๐Ÿ", "๐Ÿ", "๐Ÿ‘", "๐Ÿ’", "๐Ÿ“", "๐Ÿซ", "๐Ÿฅ"}, {"๐Ÿ˜บ", "๐Ÿ˜ธ", "๐Ÿ˜น", "๐Ÿ˜ป", "๐Ÿ˜ผ", "๐Ÿ˜ฝ", "๐Ÿ™€", "๐Ÿ˜ฟ", "๐Ÿ˜พ"}, {"๐Ÿ˜€", "๐Ÿ˜ƒ", "๐Ÿ˜„", "๐Ÿ˜", "๐Ÿ˜†", "๐Ÿ˜…", "๐Ÿคฃ", "๐Ÿ˜‚", "๐Ÿ™‚", "๐Ÿ™ƒ", "๐Ÿซ ", "๐Ÿ˜‰", "๐Ÿ˜Š", "๐Ÿ˜‡", "๐Ÿฅฐ", "๐Ÿ˜", "๐Ÿคฉ", "๐Ÿ˜˜", "๐Ÿ˜—", "โ˜บ๏ธ", "๐Ÿ˜š", "๐Ÿ˜™", "๐Ÿฅฒ", "๐Ÿ˜‹", "๐Ÿ˜›", "๐Ÿ˜œ", "๐Ÿคช", "๐Ÿ˜", "๐Ÿค‘", "๐Ÿค—", "๐Ÿคญ", "๐Ÿซข", "๐Ÿซฃ", "๐Ÿคซ", "๐Ÿค”", "๐Ÿซก", "๐Ÿค", "๐Ÿคจ", "๐Ÿ˜", "๐Ÿ˜‘", "๐Ÿ˜ถ", "๐Ÿซฅ", "๐Ÿ˜", "๐Ÿ˜’", "๐Ÿ™„", "๐Ÿ˜ฌ", "๐Ÿคฅ", "๐Ÿ˜Œ", "๐Ÿ˜”", "๐Ÿ˜ช", "๐Ÿคค", "๐Ÿ˜ด", "๐Ÿ˜ท"}, { "๐Ÿคฉ", "๐Ÿ˜", "๐Ÿฅฐ", "๐Ÿ˜˜", "๐Ÿฅณ", "๐Ÿฅฒ", "๐Ÿฅน" }, { "๐Ÿซ " }, {"๐Ÿ’˜", "๐Ÿ’", "๐Ÿ’–", "๐Ÿ’—", "๐Ÿ’“", "๐Ÿ’ž", "๐Ÿ’•", "โฃ", "๐Ÿ’”", "โค", "๐Ÿงก", "๐Ÿ’›", "๐Ÿ’š", "๐Ÿ’™", "๐Ÿ’œ", "๐ŸคŽ", "๐Ÿ–ค", "๐Ÿค"}, // {"๐Ÿ‘", "๏ธ๐Ÿซฆ", "๐Ÿ‘๏ธ"}, // this one is too much {"๐Ÿ‘ฝ", "๐Ÿ›ธ", "โœจ", "๐ŸŒŸ", "๐Ÿ’ซ", "๐Ÿš€", "๐Ÿช", "๐ŸŒ™", "โญ", "๐ŸŒ"}, {"๐ŸŒ‘", "๐ŸŒ’", "๐ŸŒ“", "๐ŸŒ”", "๐ŸŒ•", "๐ŸŒ–", "๐ŸŒ—", "๐ŸŒ˜"}, {"๐Ÿ™", "๐Ÿชธ", "๐Ÿฆ‘", "๐Ÿฆ€", "๐Ÿฆ", "๐Ÿก", "๐Ÿฆž", "๐Ÿ ", "๐ŸŸ", "๐Ÿณ", "๐Ÿ‹", "๐Ÿฌ", "๐Ÿซง", "๐ŸŒŠ", "๐Ÿฆˆ"}, {"๐Ÿ™ˆ", "๐Ÿ™‰", "๐Ÿ™Š", "๐Ÿต", "๐Ÿ’"}, {"โ™ˆ", "โ™‰", "โ™Š", "โ™‹", "โ™Œ", "โ™", "โ™Ž", "โ™", "โ™", "โ™‘", "โ™’", "โ™“"}, {"๐Ÿ•›", "๐Ÿ•ง", "๐Ÿ•", "๐Ÿ•œ", "๐Ÿ•‘", "๐Ÿ•", "๐Ÿ•’", "๐Ÿ•ž", "๐Ÿ•“", "๐Ÿ•Ÿ", "๐Ÿ•”", "๐Ÿ• ", "๐Ÿ••", "๐Ÿ•ก", "๐Ÿ•–", "๐Ÿ•ข", "๐Ÿ•—", "๐Ÿ•ฃ", "๐Ÿ•˜", "๐Ÿ•ค", "๐Ÿ•™", "๐Ÿ•ฅ", "๐Ÿ•š", "๐Ÿ•ฆ"}, {"๐ŸŒบ", "๐ŸŒธ", "๐Ÿ’ฎ", "๐Ÿต๏ธ", "๐ŸŒผ", "๐ŸŒฟ"}, {"๐Ÿข", "โœจ", "๐ŸŒŸ", "๐Ÿ‘‘"} }; static class Bubble { public float x, y, r; public int color; public String text = null; } class BubblesDrawable extends Drawable implements View.OnLongClickListener { private static final int MAX_BUBBS = 2000; private final int[] mColorIds = { android.R.color.system_accent3_400, android.R.color.system_accent3_500, android.R.color.system_accent3_600, android.R.color.system_accent2_400, android.R.color.system_accent2_500, android.R.color.system_accent2_600, }; private int[] mColors = new int[mColorIds.length]; private int mEmojiSet = -1; private final Bubble[] mBubbs = new Bubble[MAX_BUBBS]; private int mNumBubbs; private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); public float avoid = 0f; public float padding = 0f; public float minR = 0f; BubblesDrawable() { for (int i = 0; i < mColorIds.length; i++) { mColors[i] = getColor(mColorIds[i]); } for (int j = 0; j < mBubbs.length; j++) { mBubbs[j] = new Bubble(); } } @Override public void draw(Canvas canvas) { if (getLevel() == 0) return; final float f = getLevel() / 10000f; mPaint.setStyle(Paint.Style.FILL); mPaint.setTextAlign(Paint.Align.CENTER); int drawn = 0; for (int j = 0; j < mNumBubbs; j++) { if (mBubbs[j].color == 0 || mBubbs[j].r == 0) continue; if (mBubbs[j].text != null) { mPaint.setTextSize(mBubbs[j].r * 1.75f); canvas.drawText(mBubbs[j].text, mBubbs[j].x, mBubbs[j].y + mBubbs[j].r * f * 0.6f, mPaint); } else { mPaint.setColor(mBubbs[j].color); canvas.drawCircle(mBubbs[j].x, mBubbs[j].y, mBubbs[j].r * f, mPaint); } drawn++; } } public void chooseEmojiSet() { mEmojiSet = (int) (Math.random() * EMOJI_SETS.length); final String[] emojiSet = EMOJI_SETS[mEmojiSet]; for (int j = 0; j < mBubbs.length; j++) { mBubbs[j].text = emojiSet[(int) (Math.random() * emojiSet.length)]; } invalidateSelf(); } @Override protected boolean onLevelChange(int level) { invalidateSelf(); return true; } @Override protected void onBoundsChange(Rect bounds) { super.onBoundsChange(bounds); randomize(); } private void randomize() { final float w = getBounds().width(); final float h = getBounds().height(); final float maxR = Math.min(w, h) / 3f; mNumBubbs = 0; if (avoid > 0f) { mBubbs[mNumBubbs].x = w / 2f; mBubbs[mNumBubbs].y = h / 2f; mBubbs[mNumBubbs].r = avoid; mBubbs[mNumBubbs].color = 0; mNumBubbs++; } for (int j = 0; j < MAX_BUBBS; j++) { // a simple but time-tested bubble-packing algorithm: // 1. pick a spot // 2. shrink the bubble until it is no longer overlapping any other bubble // 3. if the bubble hasn't popped, keep it int tries = 5; while (tries-- > 0) { float x = (float) Math.random() * w; float y = (float) Math.random() * h; float r = Math.min(Math.min(x, w - x), Math.min(y, h - y)); // shrink radius to fit other bubbs for (int i = 0; i < mNumBubbs; i++) { r = (float) Math.min(r, Math.hypot(x - mBubbs[i].x, y - mBubbs[i].y) - mBubbs[i].r - padding); if (r < minR) break; } if (r >= minR) { // we have found a spot for this bubble to live, let's save it and move on r = Math.min(maxR, r); mBubbs[mNumBubbs].x = x; mBubbs[mNumBubbs].y = y; mBubbs[mNumBubbs].r = r; mBubbs[mNumBubbs].color = mColors[(int) (Math.random() * mColors.length)]; mNumBubbs++; break; } } } Log.v(TAG, String.format("successfully placed %d bubbles (%d%%)", mNumBubbs, (int) (100f * mNumBubbs / MAX_BUBBS))); } @Override public void setAlpha(int alpha) { } @Override public void setColorFilter(ColorFilter colorFilter) { } @Override public int getOpacity() { return TRANSLUCENT; } @Override public boolean onLongClick(View v) { if (getLevel() == 0) return false; chooseEmojiSet(); return true; } } }