1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.internal.app; 18 19 import static android.graphics.PixelFormat.TRANSLUCENT; 20 21 import android.animation.ObjectAnimator; 22 import android.app.ActionBar; 23 import android.app.Activity; 24 import android.content.ActivityNotFoundException; 25 import android.content.ContentResolver; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.graphics.Canvas; 29 import android.graphics.ColorFilter; 30 import android.graphics.Paint; 31 import android.graphics.Rect; 32 import android.graphics.drawable.Drawable; 33 import android.os.Bundle; 34 import android.provider.Settings; 35 import android.util.DisplayMetrics; 36 import android.util.Log; 37 import android.view.Gravity; 38 import android.view.HapticFeedbackConstants; 39 import android.view.MotionEvent; 40 import android.view.View; 41 import android.view.animation.DecelerateInterpolator; 42 import android.view.animation.OvershootInterpolator; 43 import android.widget.AnalogClock; 44 import android.widget.FrameLayout; 45 import android.widget.ImageView; 46 47 import com.android.internal.R; 48 49 import org.json.JSONObject; 50 51 import java.time.Clock; 52 import java.time.Instant; 53 import java.time.ZoneId; 54 import java.time.ZonedDateTime; 55 56 /** 57 * @hide 58 */ 59 public class PlatLogoActivity extends Activity { 60 private static final String TAG = "PlatLogoActivity"; 61 62 private static final String S_EGG_UNLOCK_SETTING = "egg_mode_s"; 63 64 private SettableAnalogClock mClock; 65 private ImageView mLogo; 66 private BubblesDrawable mBg; 67 68 @Override onPause()69 protected void onPause() { 70 super.onPause(); 71 } 72 73 @Override onCreate(Bundle savedInstanceState)74 protected void onCreate(Bundle savedInstanceState) { 75 super.onCreate(savedInstanceState); 76 77 getWindow().setNavigationBarColor(0); 78 getWindow().setStatusBarColor(0); 79 80 final ActionBar ab = getActionBar(); 81 if (ab != null) ab.hide(); 82 83 final FrameLayout layout = new FrameLayout(this); 84 85 mClock = new SettableAnalogClock(this); 86 87 final DisplayMetrics dm = getResources().getDisplayMetrics(); 88 final float dp = dm.density; 89 final int minSide = Math.min(dm.widthPixels, dm.heightPixels); 90 final int widgetSize = (int) (minSide * 0.75); 91 final FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(widgetSize, widgetSize); 92 lp.gravity = Gravity.CENTER; 93 layout.addView(mClock, lp); 94 95 mLogo = new ImageView(this); 96 mLogo.setVisibility(View.GONE); 97 mLogo.setImageResource(R.drawable.platlogo); 98 layout.addView(mLogo, lp); 99 100 mBg = new BubblesDrawable(); 101 mBg.setLevel(0); 102 mBg.avoid = widgetSize / 2; 103 mBg.padding = 0.5f * dp; 104 mBg.minR = 1 * dp; 105 layout.setBackground(mBg); 106 layout.setOnLongClickListener(mBg); 107 108 setContentView(layout); 109 } 110 shouldWriteSettings()111 private boolean shouldWriteSettings() { 112 return getPackageName().equals("android"); 113 } 114 launchNextStage(boolean locked)115 private void launchNextStage(boolean locked) { 116 mClock.animate() 117 .alpha(0f).scaleX(0.5f).scaleY(0.5f) 118 .withEndAction(() -> mClock.setVisibility(View.GONE)) 119 .start(); 120 121 mLogo.setAlpha(0f); 122 mLogo.setScaleX(0.5f); 123 mLogo.setScaleY(0.5f); 124 mLogo.setVisibility(View.VISIBLE); 125 mLogo.animate() 126 .alpha(1f) 127 .scaleX(1f) 128 .scaleY(1f) 129 .setInterpolator(new OvershootInterpolator()) 130 .start(); 131 132 mLogo.postDelayed(() -> { 133 final ObjectAnimator anim = ObjectAnimator.ofInt(mBg, "level", 0, 10000); 134 anim.setInterpolator(new DecelerateInterpolator(1f)); 135 anim.start(); 136 }, 137 500 138 ); 139 140 final ContentResolver cr = getContentResolver(); 141 142 try { 143 if (shouldWriteSettings()) { 144 Log.v(TAG, "Saving egg unlock=" + locked); 145 syncTouchPressure(); 146 Settings.System.putLong(cr, 147 S_EGG_UNLOCK_SETTING, 148 locked ? 0 : System.currentTimeMillis()); 149 } 150 } catch (RuntimeException e) { 151 Log.e(TAG, "Can't write settings", e); 152 } 153 154 try { 155 startActivity(new Intent(Intent.ACTION_MAIN) 156 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 157 | Intent.FLAG_ACTIVITY_CLEAR_TASK) 158 .addCategory("com.android.internal.category.PLATLOGO")); 159 } catch (ActivityNotFoundException ex) { 160 Log.e("com.android.internal.app.PlatLogoActivity", "No more eggs."); 161 } 162 //finish(); // no longer finish upon unlock; it's fun to frob the dial 163 } 164 165 static final String TOUCH_STATS = "touch.stats"; 166 double mPressureMin = 0, mPressureMax = -1; 167 measureTouchPressure(MotionEvent event)168 private void measureTouchPressure(MotionEvent event) { 169 final float pressure = event.getPressure(); 170 switch (event.getActionMasked()) { 171 case MotionEvent.ACTION_DOWN: 172 if (mPressureMax < 0) { 173 mPressureMin = mPressureMax = pressure; 174 } 175 break; 176 case MotionEvent.ACTION_MOVE: 177 if (pressure < mPressureMin) mPressureMin = pressure; 178 if (pressure > mPressureMax) mPressureMax = pressure; 179 break; 180 } 181 } 182 syncTouchPressure()183 private void syncTouchPressure() { 184 try { 185 final String touchDataJson = Settings.System.getString( 186 getContentResolver(), TOUCH_STATS); 187 final JSONObject touchData = new JSONObject( 188 touchDataJson != null ? touchDataJson : "{}"); 189 if (touchData.has("min")) { 190 mPressureMin = Math.min(mPressureMin, touchData.getDouble("min")); 191 } 192 if (touchData.has("max")) { 193 mPressureMax = Math.max(mPressureMax, touchData.getDouble("max")); 194 } 195 if (mPressureMax >= 0) { 196 touchData.put("min", mPressureMin); 197 touchData.put("max", mPressureMax); 198 if (shouldWriteSettings()) { 199 Settings.System.putString(getContentResolver(), TOUCH_STATS, 200 touchData.toString()); 201 } 202 } 203 } catch (Exception e) { 204 Log.e("com.android.internal.app.PlatLogoActivity", "Can't write touch settings", e); 205 } 206 } 207 208 @Override onStart()209 public void onStart() { 210 super.onStart(); 211 syncTouchPressure(); 212 } 213 214 @Override onStop()215 public void onStop() { 216 syncTouchPressure(); 217 super.onStop(); 218 } 219 220 /** 221 * Subclass of AnalogClock that allows the user to flip up the glass and adjust the hands. 222 */ 223 public class SettableAnalogClock extends AnalogClock { 224 private int mOverrideHour = -1; 225 private int mOverrideMinute = -1; 226 private boolean mOverride = false; 227 SettableAnalogClock(Context context)228 public SettableAnalogClock(Context context) { 229 super(context); 230 } 231 232 @Override now()233 protected Instant now() { 234 final Instant realNow = super.now(); 235 final ZoneId tz = Clock.systemDefaultZone().getZone(); 236 final ZonedDateTime zdTime = realNow.atZone(tz); 237 if (mOverride) { 238 if (mOverrideHour < 0) { 239 mOverrideHour = zdTime.getHour(); 240 } 241 return Clock.fixed(zdTime 242 .withHour(mOverrideHour) 243 .withMinute(mOverrideMinute) 244 .withSecond(0) 245 .toInstant(), tz).instant(); 246 } else { 247 return realNow; 248 } 249 } 250 toPositiveDegrees(double rad)251 double toPositiveDegrees(double rad) { 252 return (Math.toDegrees(rad) + 360 - 90) % 360; 253 } 254 255 @Override onTouchEvent(MotionEvent ev)256 public boolean onTouchEvent(MotionEvent ev) { 257 switch (ev.getActionMasked()) { 258 case MotionEvent.ACTION_DOWN: 259 mOverride = true; 260 // pass through 261 case MotionEvent.ACTION_MOVE: 262 measureTouchPressure(ev); 263 264 float x = ev.getX(); 265 float y = ev.getY(); 266 float cx = getWidth() / 2f; 267 float cy = getHeight() / 2f; 268 float angle = (float) toPositiveDegrees(Math.atan2(x - cx, y - cy)); 269 270 int minutes = (75 - (int) (angle / 6)) % 60; 271 int minuteDelta = minutes - mOverrideMinute; 272 if (minuteDelta != 0) { 273 if (Math.abs(minuteDelta) > 45 && mOverrideHour >= 0) { 274 int hourDelta = (minuteDelta < 0) ? 1 : -1; 275 mOverrideHour = (mOverrideHour + 24 + hourDelta) % 24; 276 } 277 mOverrideMinute = minutes; 278 if (mOverrideMinute == 0) { 279 performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); 280 if (getScaleX() == 1f) { 281 setScaleX(1.05f); 282 setScaleY(1.05f); 283 animate().scaleX(1f).scaleY(1f).setDuration(150).start(); 284 } 285 } else { 286 performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK); 287 } 288 289 onTimeChanged(); 290 postInvalidate(); 291 } 292 293 return true; 294 case MotionEvent.ACTION_UP: 295 if (mOverrideMinute == 0 && (mOverrideHour % 12) == 1) { 296 Log.v(TAG, "13:00"); 297 performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); 298 launchNextStage(false); 299 } 300 return true; 301 } 302 return false; 303 } 304 } 305 306 private static final String[][] EMOJI_SETS = { 307 {"", "", "", "", "", "", "", "", "", "", "", "", 308 "", "", "", ""}, 309 {"", "", "", "", "", "", "", "", ""}, 310 {"", "", "", "", "", "", "", "", "", "", "", "", "", 311 "", "", "", "", "", "", "☺️", "", "", "", "", "", "", 312 "", "", "", "", "", "", "", "", "", "", "", "", "", 313 "", "", "", "", "", "", "", "", "", "", "", "", "", 314 ""}, 315 { "", "", "", "", "", "", "" }, 316 { "" }, 317 {"", "", "", "", "", "", "", "❣", "", "❤", "", "", 318 "", "", "", "", "", ""}, 319 // {"", "️", "️"}, // this one is too much 320 {"", "", "✨", "", "", "", "", "", "⭐", ""}, 321 {"", "", "", "", "", "", "", ""}, 322 {"", "", "", "", "", "", "", "", "", "", "", "", "", "", 323 ""}, 324 {"", "", "", "", ""}, 325 {"♈", "♉", "♊", "♋", "♌", "♍", "♎", "♏", "♐", "♑", "♒", "♓"}, 326 {"", "", "", "", "", "", "", "", "", "", "", "", "", "", 327 "", "", "", "", "", "", "", "", "", ""}, 328 {"", "", "", "️", "", ""}, 329 {"", "✨", "", ""} 330 }; 331 332 static class Bubble { 333 public float x, y, r; 334 public int color; 335 public String text = null; 336 } 337 338 class BubblesDrawable extends Drawable implements View.OnLongClickListener { 339 private static final int MAX_BUBBS = 2000; 340 341 private final int[] mColorIds = { 342 android.R.color.system_accent3_400, 343 android.R.color.system_accent3_500, 344 android.R.color.system_accent3_600, 345 346 android.R.color.system_accent2_400, 347 android.R.color.system_accent2_500, 348 android.R.color.system_accent2_600, 349 }; 350 351 private int[] mColors = new int[mColorIds.length]; 352 353 private int mEmojiSet = -1; 354 355 private final Bubble[] mBubbs = new Bubble[MAX_BUBBS]; 356 private int mNumBubbs; 357 358 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 359 360 public float avoid = 0f; 361 public float padding = 0f; 362 public float minR = 0f; 363 BubblesDrawable()364 BubblesDrawable() { 365 for (int i = 0; i < mColorIds.length; i++) { 366 mColors[i] = getColor(mColorIds[i]); 367 } 368 for (int j = 0; j < mBubbs.length; j++) { 369 mBubbs[j] = new Bubble(); 370 } 371 } 372 373 @Override draw(Canvas canvas)374 public void draw(Canvas canvas) { 375 if (getLevel() == 0) return; 376 final float f = getLevel() / 10000f; 377 mPaint.setStyle(Paint.Style.FILL); 378 mPaint.setTextAlign(Paint.Align.CENTER); 379 int drawn = 0; 380 for (int j = 0; j < mNumBubbs; j++) { 381 if (mBubbs[j].color == 0 || mBubbs[j].r == 0) continue; 382 if (mBubbs[j].text != null) { 383 mPaint.setTextSize(mBubbs[j].r * 1.75f); 384 canvas.drawText(mBubbs[j].text, mBubbs[j].x, 385 mBubbs[j].y + mBubbs[j].r * f * 0.6f, mPaint); 386 } else { 387 mPaint.setColor(mBubbs[j].color); 388 canvas.drawCircle(mBubbs[j].x, mBubbs[j].y, mBubbs[j].r * f, mPaint); 389 } 390 drawn++; 391 } 392 } 393 chooseEmojiSet()394 public void chooseEmojiSet() { 395 mEmojiSet = (int) (Math.random() * EMOJI_SETS.length); 396 final String[] emojiSet = EMOJI_SETS[mEmojiSet]; 397 for (int j = 0; j < mBubbs.length; j++) { 398 mBubbs[j].text = emojiSet[(int) (Math.random() * emojiSet.length)]; 399 } 400 invalidateSelf(); 401 } 402 403 @Override onLevelChange(int level)404 protected boolean onLevelChange(int level) { 405 invalidateSelf(); 406 return true; 407 } 408 409 @Override onBoundsChange(Rect bounds)410 protected void onBoundsChange(Rect bounds) { 411 super.onBoundsChange(bounds); 412 randomize(); 413 } 414 randomize()415 private void randomize() { 416 final float w = getBounds().width(); 417 final float h = getBounds().height(); 418 final float maxR = Math.min(w, h) / 3f; 419 mNumBubbs = 0; 420 if (avoid > 0f) { 421 mBubbs[mNumBubbs].x = w / 2f; 422 mBubbs[mNumBubbs].y = h / 2f; 423 mBubbs[mNumBubbs].r = avoid; 424 mBubbs[mNumBubbs].color = 0; 425 mNumBubbs++; 426 } 427 for (int j = 0; j < MAX_BUBBS; j++) { 428 // a simple but time-tested bubble-packing algorithm: 429 // 1. pick a spot 430 // 2. shrink the bubble until it is no longer overlapping any other bubble 431 // 3. if the bubble hasn't popped, keep it 432 int tries = 5; 433 while (tries-- > 0) { 434 float x = (float) Math.random() * w; 435 float y = (float) Math.random() * h; 436 float r = Math.min(Math.min(x, w - x), Math.min(y, h - y)); 437 438 // shrink radius to fit other bubbs 439 for (int i = 0; i < mNumBubbs; i++) { 440 r = (float) Math.min(r, 441 Math.hypot(x - mBubbs[i].x, y - mBubbs[i].y) - mBubbs[i].r 442 - padding); 443 if (r < minR) break; 444 } 445 446 if (r >= minR) { 447 // we have found a spot for this bubble to live, let's save it and move on 448 r = Math.min(maxR, r); 449 450 mBubbs[mNumBubbs].x = x; 451 mBubbs[mNumBubbs].y = y; 452 mBubbs[mNumBubbs].r = r; 453 mBubbs[mNumBubbs].color = mColors[(int) (Math.random() * mColors.length)]; 454 mNumBubbs++; 455 break; 456 } 457 } 458 } 459 Log.v(TAG, String.format("successfully placed %d bubbles (%d%%)", 460 mNumBubbs, (int) (100f * mNumBubbs / MAX_BUBBS))); 461 } 462 463 @Override setAlpha(int alpha)464 public void setAlpha(int alpha) { } 465 466 @Override setColorFilter(ColorFilter colorFilter)467 public void setColorFilter(ColorFilter colorFilter) { } 468 469 @Override getOpacity()470 public int getOpacity() { 471 return TRANSLUCENT; 472 } 473 474 @Override onLongClick(View v)475 public boolean onLongClick(View v) { 476 if (getLevel() == 0) return false; 477 chooseEmojiSet(); 478 return true; 479 } 480 } 481 482 } 483