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.os.VibrationEffect.Composition.PRIMITIVE_SPIN; 20 21 import static java.lang.Math.hypot; 22 import static java.lang.Math.max; 23 24 import android.animation.ObjectAnimator; 25 import android.animation.TimeAnimator; 26 import android.annotation.SuppressLint; 27 import android.app.ActionBar; 28 import android.app.Activity; 29 import android.content.ActivityNotFoundException; 30 import android.content.ContentResolver; 31 import android.content.Intent; 32 import android.content.pm.ActivityInfo; 33 import android.graphics.Canvas; 34 import android.graphics.Color; 35 import android.graphics.ColorFilter; 36 import android.graphics.ColorSpace; 37 import android.graphics.Paint; 38 import android.graphics.PixelFormat; 39 import android.graphics.Rect; 40 import android.graphics.drawable.Drawable; 41 import android.os.Bundle; 42 import android.os.CombinedVibration; 43 import android.os.Handler; 44 import android.os.HandlerThread; 45 import android.os.Message; 46 import android.os.VibrationEffect; 47 import android.os.VibratorManager; 48 import android.provider.Settings; 49 import android.util.DisplayMetrics; 50 import android.util.Log; 51 import android.view.Gravity; 52 import android.view.HapticFeedbackConstants; 53 import android.view.KeyEvent; 54 import android.view.MotionEvent; 55 import android.view.View; 56 import android.view.WindowInsets; 57 import android.widget.FrameLayout; 58 import android.widget.ImageView; 59 60 import androidx.annotation.NonNull; 61 import androidx.annotation.Nullable; 62 63 import com.android.internal.R; 64 65 import org.json.JSONObject; 66 67 import java.util.Random; 68 69 /** 70 * @hide 71 */ 72 public class PlatLogoActivity extends Activity { 73 private static final String TAG = "PlatLogoActivity"; 74 75 private static final long LAUNCH_TIME = 5000L; 76 77 private static final String EGG_UNLOCK_SETTING = "egg_mode_v"; 78 79 private static final float MIN_WARP = 1f; 80 private static final float MAX_WARP = 16f; // must go faster 81 private static final boolean FINISH_AFTER_NEXT_STAGE_LAUNCH = false; 82 83 private ImageView mLogo; 84 private Starfield mStarfield; 85 86 private FrameLayout mLayout; 87 88 private TimeAnimator mAnim; 89 private ObjectAnimator mWarpAnim; 90 private Random mRandom; 91 private float mDp; 92 93 private RumblePack mRumble; 94 95 private boolean mAnimationsEnabled = true; 96 97 private final View.OnTouchListener mTouchListener = new View.OnTouchListener() { 98 @Override 99 public boolean onTouch(View v, MotionEvent event) { 100 switch (event.getActionMasked()) { 101 case MotionEvent.ACTION_DOWN: 102 measureTouchPressure(event); 103 startWarp(); 104 break; 105 case MotionEvent.ACTION_UP: 106 case MotionEvent.ACTION_CANCEL: 107 stopWarp(); 108 break; 109 } 110 return true; 111 } 112 113 }; 114 115 private final Runnable mLaunchNextStage = () -> { 116 stopWarp(); 117 launchNextStage(false); 118 }; 119 120 private final TimeAnimator.TimeListener mTimeListener = new TimeAnimator.TimeListener() { 121 @Override 122 public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) { 123 mStarfield.update(deltaTime); 124 final float warpFrac = (mStarfield.getWarp() - MIN_WARP) / (MAX_WARP - MIN_WARP); 125 if (mAnimationsEnabled) { 126 mLogo.setTranslationX(mRandom.nextFloat() * warpFrac * 5 * mDp); 127 mLogo.setTranslationY(mRandom.nextFloat() * warpFrac * 5 * mDp); 128 } 129 if (warpFrac > 0f) { 130 mRumble.rumble(warpFrac); 131 } 132 mLayout.postInvalidate(); 133 } 134 }; 135 136 private class RumblePack implements Handler.Callback { 137 private static final int MSG = 6464; 138 private static final int INTERVAL = 50; 139 140 private final VibratorManager mVibeMan; 141 private final HandlerThread mVibeThread; 142 private final Handler mVibeHandler; 143 private boolean mSpinPrimitiveSupported; 144 145 private long mLastVibe = 0; 146 147 @SuppressLint("MissingPermission") 148 @Override handleMessage(Message msg)149 public boolean handleMessage(Message msg) { 150 final float warpFrac = msg.arg1 / 100f; 151 if (mSpinPrimitiveSupported) { 152 if (msg.getWhen() > mLastVibe + INTERVAL) { 153 mLastVibe = msg.getWhen(); 154 mVibeMan.vibrate(CombinedVibration.createParallel( 155 VibrationEffect.startComposition() 156 .addPrimitive(PRIMITIVE_SPIN, (float) Math.pow(warpFrac, 3.0)) 157 .compose() 158 )); 159 } 160 } else { 161 if (mRandom.nextFloat() < warpFrac) { 162 mLogo.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK); 163 } 164 } 165 return false; 166 } RumblePack()167 RumblePack() { 168 mVibeMan = getSystemService(VibratorManager.class); 169 mSpinPrimitiveSupported = mVibeMan.getDefaultVibrator() 170 .areAllPrimitivesSupported(PRIMITIVE_SPIN); 171 172 mVibeThread = new HandlerThread("VibratorThread"); 173 mVibeThread.start(); 174 mVibeHandler = Handler.createAsync(mVibeThread.getLooper(), this); 175 } 176 destroy()177 public void destroy() { 178 mVibeThread.quit(); 179 } 180 rumble(float warpFrac)181 private void rumble(float warpFrac) { 182 if (!mVibeThread.isAlive()) return; 183 184 final Message msg = Message.obtain(); 185 msg.what = MSG; 186 msg.arg1 = (int) (warpFrac * 100); 187 mVibeHandler.removeMessages(MSG); 188 mVibeHandler.sendMessage(msg); 189 } 190 191 } 192 193 @Override onDestroy()194 protected void onDestroy() { 195 mRumble.destroy(); 196 197 super.onDestroy(); 198 } 199 200 @Override onCreate(Bundle savedInstanceState)201 protected void onCreate(Bundle savedInstanceState) { 202 super.onCreate(savedInstanceState); 203 204 getWindow().setDecorFitsSystemWindows(false); 205 getWindow().setNavigationBarColor(0); 206 getWindow().setStatusBarColor(0); 207 getWindow().getDecorView().getWindowInsetsController().hide(WindowInsets.Type.systemBars()); 208 209 // This will be silently ignored on displays that don't support HDR color, which is fine 210 getWindow().setColorMode(ActivityInfo.COLOR_MODE_HDR); 211 212 final ActionBar ab = getActionBar(); 213 if (ab != null) ab.hide(); 214 215 try { 216 mAnimationsEnabled = Settings.Global.getFloat(getContentResolver(), 217 Settings.Global.ANIMATOR_DURATION_SCALE) > 0f; 218 } catch (Settings.SettingNotFoundException e) { 219 mAnimationsEnabled = true; 220 } 221 222 mRumble = new RumblePack(); 223 224 mLayout = new FrameLayout(this); 225 mRandom = new Random(); 226 mDp = getResources().getDisplayMetrics().density; 227 mStarfield = new Starfield(mRandom, mDp * 2f); 228 mStarfield.setVelocity( 229 200f * (mRandom.nextFloat() - 0.5f), 230 200f * (mRandom.nextFloat() - 0.5f)); 231 mLayout.setBackground(mStarfield); 232 233 final DisplayMetrics dm = getResources().getDisplayMetrics(); 234 final float dp = dm.density; 235 final int minSide = Math.min(dm.widthPixels, dm.heightPixels); 236 final int widgetSize = (int) (minSide * 0.75); 237 final FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(widgetSize, widgetSize); 238 lp.gravity = Gravity.CENTER; 239 240 mLogo = new ImageView(this); 241 mLogo.setImageResource(R.drawable.platlogo); 242 mLogo.setOnTouchListener(mTouchListener); 243 mLogo.requestFocus(); 244 mLayout.addView(mLogo, lp); 245 246 Log.v(TAG, "Hello"); 247 248 setContentView(mLayout); 249 } 250 startAnimating()251 private void startAnimating() { 252 mAnim = new TimeAnimator(); 253 mAnim.setTimeListener(mTimeListener); 254 mAnim.start(); 255 } 256 stopAnimating()257 private void stopAnimating() { 258 mAnim.cancel(); 259 mAnim = null; 260 } 261 262 @Override onKeyDown(int keyCode, KeyEvent event)263 public boolean onKeyDown(int keyCode, KeyEvent event) { 264 if (keyCode == KeyEvent.KEYCODE_SPACE) { 265 if (event.getRepeatCount() == 0) { 266 startWarp(); 267 } 268 return true; 269 } 270 return super.onKeyDown(keyCode,event); 271 } 272 273 @Override onKeyUp(int keyCode, KeyEvent event)274 public boolean onKeyUp(int keyCode, KeyEvent event) { 275 if (keyCode == KeyEvent.KEYCODE_SPACE) { 276 stopWarp(); 277 return true; 278 } 279 return super.onKeyUp(keyCode,event); 280 } 281 startWarp()282 private void startWarp() { 283 stopWarp(); 284 mWarpAnim = ObjectAnimator.ofFloat(mStarfield, "warp", MIN_WARP, MAX_WARP) 285 .setDuration(LAUNCH_TIME); 286 mWarpAnim.start(); 287 288 mLogo.postDelayed(mLaunchNextStage, LAUNCH_TIME + 1000L); 289 } 290 stopWarp()291 private void stopWarp() { 292 if (mWarpAnim != null) { 293 mWarpAnim.cancel(); 294 mWarpAnim.removeAllListeners(); 295 mWarpAnim = null; 296 } 297 mStarfield.setWarp(1f); 298 mLogo.removeCallbacks(mLaunchNextStage); 299 } 300 301 @Override onResume()302 public void onResume() { 303 super.onResume(); 304 startAnimating(); 305 } 306 307 @Override onPause()308 public void onPause() { 309 stopWarp(); 310 stopAnimating(); 311 super.onPause(); 312 } 313 shouldWriteSettings()314 private boolean shouldWriteSettings() { 315 return getPackageName().equals("android"); 316 } 317 launchNextStage(boolean locked)318 private void launchNextStage(boolean locked) { 319 final ContentResolver cr = getContentResolver(); 320 try { 321 if (shouldWriteSettings()) { 322 Log.v(TAG, "Saving egg locked=" + locked); 323 syncTouchPressure(); 324 Settings.System.putLong(cr, 325 EGG_UNLOCK_SETTING, 326 locked ? 0 : System.currentTimeMillis()); 327 } 328 } catch (RuntimeException e) { 329 Log.e(TAG, "Can't write settings", e); 330 } 331 332 try { 333 final Intent eggActivity = new Intent(Intent.ACTION_MAIN) 334 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 335 | Intent.FLAG_ACTIVITY_CLEAR_TASK) 336 .addCategory("com.android.internal.category.PLATLOGO"); 337 Log.v(TAG, "launching: " + eggActivity); 338 startActivity(eggActivity); 339 } catch (ActivityNotFoundException ex) { 340 Log.e("com.android.internal.app.PlatLogoActivity", "No more eggs."); 341 } 342 if (FINISH_AFTER_NEXT_STAGE_LAUNCH) { 343 finish(); // we're done here. 344 } 345 } 346 347 static final String TOUCH_STATS = "touch.stats"; 348 double mPressureMin = 0, mPressureMax = -1; 349 measureTouchPressure(MotionEvent event)350 private void measureTouchPressure(MotionEvent event) { 351 final float pressure = event.getPressure(); 352 switch (event.getActionMasked()) { 353 case MotionEvent.ACTION_DOWN: 354 if (mPressureMax < 0) { 355 mPressureMin = mPressureMax = pressure; 356 } 357 break; 358 case MotionEvent.ACTION_MOVE: 359 if (pressure < mPressureMin) mPressureMin = pressure; 360 if (pressure > mPressureMax) mPressureMax = pressure; 361 break; 362 } 363 } 364 syncTouchPressure()365 private void syncTouchPressure() { 366 try { 367 final String touchDataJson = Settings.System.getString( 368 getContentResolver(), TOUCH_STATS); 369 final JSONObject touchData = new JSONObject( 370 touchDataJson != null ? touchDataJson : "{}"); 371 if (touchData.has("min")) { 372 mPressureMin = Math.min(mPressureMin, touchData.getDouble("min")); 373 } 374 if (touchData.has("max")) { 375 mPressureMax = max(mPressureMax, touchData.getDouble("max")); 376 } 377 if (mPressureMax >= 0) { 378 touchData.put("min", mPressureMin); 379 touchData.put("max", mPressureMax); 380 if (shouldWriteSettings()) { 381 Settings.System.putString(getContentResolver(), TOUCH_STATS, 382 touchData.toString()); 383 } 384 } 385 } catch (Exception e) { 386 Log.e("com.android.internal.app.PlatLogoActivity", "Can't write touch settings", e); 387 } 388 } 389 390 @Override onStart()391 public void onStart() { 392 super.onStart(); 393 syncTouchPressure(); 394 } 395 396 @Override onStop()397 public void onStop() { 398 syncTouchPressure(); 399 super.onStop(); 400 } 401 402 private static class Starfield extends Drawable { 403 private static final int NUM_STARS = 128; 404 405 private static final int NUM_PLANES = 4; 406 407 private static final float ROTATION = 45; 408 private final float[] mStars = new float[NUM_STARS * 4]; 409 private float mVx, mVy; 410 private long mDt = 0; 411 private final Paint mStarPaint; 412 413 private final Random mRng; 414 private final float mSize; 415 416 private float mRadius = 0f; 417 private float mWarp = 1f; 418 419 private float mBuffer; 420 setWarp(float warp)421 public void setWarp(float warp) { 422 mWarp = warp; 423 } 424 getWarp()425 public float getWarp() { 426 return mWarp; 427 } 428 Starfield(Random rng, float size)429 Starfield(Random rng, float size) { 430 mRng = rng; 431 mSize = size; 432 mStarPaint = new Paint(); 433 mStarPaint.setStyle(Paint.Style.STROKE); 434 mStarPaint.setColor(Color.WHITE); 435 } 436 437 @Override onBoundsChange(Rect bounds)438 public void onBoundsChange(Rect bounds) { 439 mBuffer = mSize * NUM_PLANES * 2 * MAX_WARP; 440 mRadius = ((float) hypot(bounds.width(), bounds.height()) / 2f) + mBuffer; 441 // I didn't clarify this the last time, but we store both the beginning and 442 // end of each star's trail in this data structure. When we're not in warp that means 443 // that we've got each star in there twice. It's fine, we're gonna move it off-screen 444 for (int i = 0; i < NUM_STARS; i++) { 445 mStars[4 * i] = mRng.nextFloat() * 2 * mRadius - mRadius; 446 mStars[4 * i + 1] = mRng.nextFloat() * 2 * mRadius - mRadius; 447 // duplicate copy (for now) 448 mStars[4 * i + 2] = mStars[4 * i]; 449 mStars[4 * i + 3] = mStars[4 * i + 1]; 450 } 451 } 452 setVelocity(float x, float y)453 public void setVelocity(float x, float y) { 454 mVx = x; 455 mVy = y; 456 } 457 458 @Override draw(@onNull Canvas canvas)459 public void draw(@NonNull Canvas canvas) { 460 final float dtSec = mDt / 1000f; 461 final float dx = (mVx * dtSec * mWarp); 462 final float dy = (mVy * dtSec * mWarp); 463 464 final boolean inWarp = mWarp > 1f; 465 466 final float diameter = mRadius * 2f; 467 final float triameter = mRadius * 3f; 468 469 canvas.drawColor(Color.BLACK); 470 471 final float cx = getBounds().width() / 2f; 472 final float cy = getBounds().height() / 2f; 473 canvas.translate(cx, cy); 474 475 canvas.rotate(ROTATION); 476 477 if (mDt > 0 && mDt < 1000) { 478 canvas.translate( 479 mRng.nextFloat() * (mWarp - 1f), 480 mRng.nextFloat() * (mWarp - 1f) 481 ); 482 for (int i = 0; i < NUM_STARS; i++) { 483 final int plane = (int) ((((float) i) / NUM_STARS) * NUM_PLANES) + 1; 484 mStars[4 * i + 2] = (mStars[4 * i + 2] + dx * plane + triameter) % diameter 485 - mRadius; 486 mStars[4 * i + 3] = (mStars[4 * i + 3] + dy * plane + triameter) % diameter 487 - mRadius; 488 mStars[4 * i + 0] = inWarp ? mStars[4 * i + 2] - dx * mWarp * plane : -10000; 489 mStars[4 * i + 1] = inWarp ? mStars[4 * i + 3] - dy * mWarp * plane : -10000; 490 } 491 } 492 final int slice = (mStars.length / NUM_PLANES / 4) * 4; 493 for (int p = 0; p < NUM_PLANES; p++) { 494 final float value = (p + 1f) / (NUM_PLANES - 1); 495 mStarPaint.setColor(packHdrColor(value, 1.0f)); 496 mStarPaint.setStrokeWidth(mSize * (p + 1)); 497 if (inWarp) { 498 canvas.drawLines(mStars, p * slice, slice, mStarPaint); 499 } 500 canvas.drawPoints(mStars, p * slice, slice, mStarPaint); 501 } 502 503 if (inWarp) { 504 final float frac = (mWarp - MIN_WARP) / (MAX_WARP - MIN_WARP); 505 canvas.drawColor(packHdrColor(2.0f, frac * frac)); 506 } 507 } 508 509 @Override setAlpha(int alpha)510 public void setAlpha(int alpha) { 511 512 } 513 514 @Override setColorFilter(@ullable ColorFilter colorFilter)515 public void setColorFilter(@Nullable ColorFilter colorFilter) { 516 517 } 518 519 @Override getOpacity()520 public int getOpacity() { 521 return PixelFormat.OPAQUE; 522 } 523 update(long dt)524 public void update(long dt) { 525 mDt = dt; 526 } 527 528 private static final ColorSpace sSrgbExt = ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB); packHdrColor(float value, float alpha)529 public static long packHdrColor(float value, float alpha) { 530 return Color.valueOf(value, value, value, alpha, sSrgbExt).pack(); 531 } 532 } 533 } 534