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