| /* |
| * 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.os.VibrationEffect.Composition.PRIMITIVE_SPIN; |
| |
| import android.animation.ObjectAnimator; |
| import android.animation.TimeAnimator; |
| import android.annotation.SuppressLint; |
| import android.app.ActionBar; |
| import android.app.Activity; |
| import android.content.ActivityNotFoundException; |
| import android.content.ContentResolver; |
| import android.content.Intent; |
| import android.graphics.Canvas; |
| import android.graphics.Color; |
| import android.graphics.ColorFilter; |
| import android.graphics.Paint; |
| import android.graphics.PixelFormat; |
| import android.graphics.Rect; |
| import android.graphics.drawable.Drawable; |
| import android.os.Bundle; |
| import android.os.CombinedVibration; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.Message; |
| import android.os.VibrationEffect; |
| import android.os.VibratorManager; |
| import android.provider.Settings; |
| import android.util.DisplayMetrics; |
| import android.util.Log; |
| import android.view.Gravity; |
| import android.view.HapticFeedbackConstants; |
| import android.view.KeyEvent; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.WindowInsets; |
| import android.widget.FrameLayout; |
| import android.widget.ImageView; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| |
| import com.android.internal.R; |
| |
| import org.json.JSONObject; |
| |
| import java.util.Random; |
| |
| /** |
| * @hide |
| */ |
| public class PlatLogoActivity extends Activity { |
| private static final String TAG = "PlatLogoActivity"; |
| |
| private static final long LAUNCH_TIME = 5000L; |
| |
| private static final String U_EGG_UNLOCK_SETTING = "egg_mode_u"; |
| |
| private static final float MIN_WARP = 1f; |
| private static final float MAX_WARP = 10f; // after all these years |
| private static final boolean FINISH_AFTER_NEXT_STAGE_LAUNCH = false; |
| |
| private ImageView mLogo; |
| private Starfield mStarfield; |
| |
| private FrameLayout mLayout; |
| |
| private TimeAnimator mAnim; |
| private ObjectAnimator mWarpAnim; |
| private Random mRandom; |
| private float mDp; |
| |
| private RumblePack mRumble; |
| |
| private boolean mAnimationsEnabled = true; |
| |
| private final View.OnTouchListener mTouchListener = new View.OnTouchListener() { |
| @Override |
| public boolean onTouch(View v, MotionEvent event) { |
| switch (event.getActionMasked()) { |
| case MotionEvent.ACTION_DOWN: |
| measureTouchPressure(event); |
| startWarp(); |
| break; |
| case MotionEvent.ACTION_UP: |
| case MotionEvent.ACTION_CANCEL: |
| stopWarp(); |
| break; |
| } |
| return true; |
| } |
| |
| }; |
| |
| private final Runnable mLaunchNextStage = () -> { |
| stopWarp(); |
| launchNextStage(false); |
| }; |
| |
| private final TimeAnimator.TimeListener mTimeListener = new TimeAnimator.TimeListener() { |
| @Override |
| public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) { |
| mStarfield.update(deltaTime); |
| final float warpFrac = (mStarfield.getWarp() - MIN_WARP) / (MAX_WARP - MIN_WARP); |
| if (mAnimationsEnabled) { |
| mLogo.setTranslationX(mRandom.nextFloat() * warpFrac * 5 * mDp); |
| mLogo.setTranslationY(mRandom.nextFloat() * warpFrac * 5 * mDp); |
| } |
| if (warpFrac > 0f) { |
| mRumble.rumble(warpFrac); |
| } |
| mLayout.postInvalidate(); |
| } |
| }; |
| |
| private class RumblePack implements Handler.Callback { |
| private static final int MSG = 6464; |
| private static final int INTERVAL = 50; |
| |
| private final VibratorManager mVibeMan; |
| private final HandlerThread mVibeThread; |
| private final Handler mVibeHandler; |
| private boolean mSpinPrimitiveSupported; |
| |
| private long mLastVibe = 0; |
| |
| @SuppressLint("MissingPermission") |
| @Override |
| public boolean handleMessage(Message msg) { |
| final float warpFrac = msg.arg1 / 100f; |
| if (mSpinPrimitiveSupported) { |
| if (msg.getWhen() > mLastVibe + INTERVAL) { |
| mLastVibe = msg.getWhen(); |
| mVibeMan.vibrate(CombinedVibration.createParallel( |
| VibrationEffect.startComposition() |
| .addPrimitive(PRIMITIVE_SPIN, (float) Math.pow(warpFrac, 3.0)) |
| .compose() |
| )); |
| } |
| } else { |
| if (mRandom.nextFloat() < warpFrac) { |
| mLogo.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK); |
| } |
| } |
| return false; |
| } |
| RumblePack() { |
| mVibeMan = getSystemService(VibratorManager.class); |
| mSpinPrimitiveSupported = mVibeMan.getDefaultVibrator() |
| .areAllPrimitivesSupported(PRIMITIVE_SPIN); |
| |
| mVibeThread = new HandlerThread("VibratorThread"); |
| mVibeThread.start(); |
| mVibeHandler = Handler.createAsync(mVibeThread.getLooper(), this); |
| } |
| |
| public void destroy() { |
| mVibeThread.quit(); |
| } |
| |
| private void rumble(float warpFrac) { |
| if (!mVibeThread.isAlive()) return; |
| |
| final Message msg = Message.obtain(); |
| msg.what = MSG; |
| msg.arg1 = (int) (warpFrac * 100); |
| mVibeHandler.removeMessages(MSG); |
| mVibeHandler.sendMessage(msg); |
| } |
| |
| } |
| |
| @Override |
| protected void onDestroy() { |
| mRumble.destroy(); |
| |
| super.onDestroy(); |
| } |
| |
| @Override |
| protected void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| |
| getWindow().setDecorFitsSystemWindows(false); |
| getWindow().setNavigationBarColor(0); |
| getWindow().setStatusBarColor(0); |
| getWindow().getDecorView().getWindowInsetsController().hide(WindowInsets.Type.systemBars()); |
| |
| final ActionBar ab = getActionBar(); |
| if (ab != null) ab.hide(); |
| |
| try { |
| mAnimationsEnabled = Settings.Global.getFloat(getContentResolver(), |
| Settings.Global.ANIMATOR_DURATION_SCALE) > 0f; |
| } catch (Settings.SettingNotFoundException e) { |
| mAnimationsEnabled = true; |
| } |
| |
| mRumble = new RumblePack(); |
| |
| mLayout = new FrameLayout(this); |
| mRandom = new Random(); |
| mDp = getResources().getDisplayMetrics().density; |
| mStarfield = new Starfield(mRandom, mDp * 2f); |
| mStarfield.setVelocity( |
| 200f * (mRandom.nextFloat() - 0.5f), |
| 200f * (mRandom.nextFloat() - 0.5f)); |
| mLayout.setBackground(mStarfield); |
| |
| 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; |
| |
| mLogo = new ImageView(this); |
| mLogo.setImageResource(R.drawable.platlogo); |
| mLogo.setOnTouchListener(mTouchListener); |
| mLogo.requestFocus(); |
| mLayout.addView(mLogo, lp); |
| |
| Log.v(TAG, "Hello"); |
| |
| setContentView(mLayout); |
| } |
| |
| private void startAnimating() { |
| mAnim = new TimeAnimator(); |
| mAnim.setTimeListener(mTimeListener); |
| mAnim.start(); |
| } |
| |
| private void stopAnimating() { |
| mAnim.cancel(); |
| mAnim = null; |
| } |
| |
| @Override |
| public boolean onKeyDown(int keyCode, KeyEvent event) { |
| if (keyCode == KeyEvent.KEYCODE_SPACE) { |
| if (event.getRepeatCount() == 0) { |
| startWarp(); |
| } |
| return true; |
| } |
| return super.onKeyDown(keyCode,event); |
| } |
| |
| @Override |
| public boolean onKeyUp(int keyCode, KeyEvent event) { |
| if (keyCode == KeyEvent.KEYCODE_SPACE) { |
| stopWarp(); |
| return true; |
| } |
| return super.onKeyUp(keyCode,event); |
| } |
| |
| private void startWarp() { |
| stopWarp(); |
| mWarpAnim = ObjectAnimator.ofFloat(mStarfield, "warp", MIN_WARP, MAX_WARP) |
| .setDuration(LAUNCH_TIME); |
| mWarpAnim.start(); |
| |
| mLogo.postDelayed(mLaunchNextStage, LAUNCH_TIME + 1000L); |
| } |
| |
| private void stopWarp() { |
| if (mWarpAnim != null) { |
| mWarpAnim.cancel(); |
| mWarpAnim.removeAllListeners(); |
| mWarpAnim = null; |
| } |
| mStarfield.setWarp(1f); |
| mLogo.removeCallbacks(mLaunchNextStage); |
| } |
| |
| @Override |
| public void onResume() { |
| super.onResume(); |
| startAnimating(); |
| } |
| |
| @Override |
| public void onPause() { |
| stopWarp(); |
| stopAnimating(); |
| super.onPause(); |
| } |
| |
| private boolean shouldWriteSettings() { |
| return getPackageName().equals("android"); |
| } |
| |
| private void launchNextStage(boolean locked) { |
| final ContentResolver cr = getContentResolver(); |
| |
| try { |
| if (shouldWriteSettings()) { |
| Log.v(TAG, "Saving egg locked=" + locked); |
| syncTouchPressure(); |
| Settings.System.putLong(cr, |
| U_EGG_UNLOCK_SETTING, |
| locked ? 0 : System.currentTimeMillis()); |
| } |
| } catch (RuntimeException e) { |
| Log.e(TAG, "Can't write settings", e); |
| } |
| |
| try { |
| final Intent eggActivity = new Intent(Intent.ACTION_MAIN) |
| .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
| | Intent.FLAG_ACTIVITY_CLEAR_TASK) |
| .addCategory("com.android.internal.category.PLATLOGO"); |
| Log.v(TAG, "launching: " + eggActivity); |
| startActivity(eggActivity); |
| } catch (ActivityNotFoundException ex) { |
| Log.e("com.android.internal.app.PlatLogoActivity", "No more eggs."); |
| } |
| if (FINISH_AFTER_NEXT_STAGE_LAUNCH) { |
| finish(); // we're done here. |
| } |
| } |
| |
| 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(); |
| } |
| |
| private static class Starfield extends Drawable { |
| private static final int NUM_STARS = 34; // Build.VERSION_CODES.UPSIDE_DOWN_CAKE |
| |
| private static final int NUM_PLANES = 2; |
| private final float[] mStars = new float[NUM_STARS * 4]; |
| private float mVx, mVy; |
| private long mDt = 0; |
| private final Paint mStarPaint; |
| |
| private final Random mRng; |
| private final float mSize; |
| |
| private final Rect mSpace = new Rect(); |
| private float mWarp = 1f; |
| |
| private float mBuffer; |
| |
| public void setWarp(float warp) { |
| mWarp = warp; |
| } |
| |
| public float getWarp() { |
| return mWarp; |
| } |
| |
| Starfield(Random rng, float size) { |
| mRng = rng; |
| mSize = size; |
| mStarPaint = new Paint(); |
| mStarPaint.setStyle(Paint.Style.STROKE); |
| mStarPaint.setColor(Color.WHITE); |
| } |
| |
| @Override |
| public void onBoundsChange(Rect bounds) { |
| mSpace.set(bounds); |
| mBuffer = mSize * NUM_PLANES * 2 * MAX_WARP; |
| mSpace.inset(-(int) mBuffer, -(int) mBuffer); |
| final float w = mSpace.width(); |
| final float h = mSpace.height(); |
| for (int i = 0; i < NUM_STARS; i++) { |
| mStars[4 * i] = mRng.nextFloat() * w; |
| mStars[4 * i + 1] = mRng.nextFloat() * h; |
| mStars[4 * i + 2] = mStars[4 * i]; |
| mStars[4 * i + 3] = mStars[4 * i + 1]; |
| } |
| } |
| |
| public void setVelocity(float x, float y) { |
| mVx = x; |
| mVy = y; |
| } |
| |
| @Override |
| public void draw(@NonNull Canvas canvas) { |
| final float dtSec = mDt / 1000f; |
| final float dx = (mVx * dtSec * mWarp); |
| final float dy = (mVy * dtSec * mWarp); |
| |
| final boolean inWarp = mWarp > 1f; |
| |
| canvas.drawColor(Color.BLACK); // 0xFF16161D); |
| |
| if (mDt > 0 && mDt < 1000) { |
| canvas.translate( |
| -(mBuffer) + mRng.nextFloat() * (mWarp - 1f), |
| -(mBuffer) + mRng.nextFloat() * (mWarp - 1f) |
| ); |
| final float w = mSpace.width(); |
| final float h = mSpace.height(); |
| for (int i = 0; i < NUM_STARS; i++) { |
| final int plane = (int) ((((float) i) / NUM_STARS) * NUM_PLANES) + 1; |
| mStars[4 * i + 2] = (mStars[4 * i + 2] + dx * plane + w) % w; |
| mStars[4 * i + 3] = (mStars[4 * i + 3] + dy * plane + h) % h; |
| mStars[4 * i + 0] = inWarp ? mStars[4 * i + 2] - dx * mWarp * 2 * plane : -100; |
| mStars[4 * i + 1] = inWarp ? mStars[4 * i + 3] - dy * mWarp * 2 * plane : -100; |
| } |
| } |
| final int slice = (mStars.length / NUM_PLANES / 4) * 4; |
| for (int p = 0; p < NUM_PLANES; p++) { |
| mStarPaint.setStrokeWidth(mSize * (p + 1)); |
| if (inWarp) { |
| canvas.drawLines(mStars, p * slice, slice, mStarPaint); |
| } |
| canvas.drawPoints(mStars, p * slice, slice, mStarPaint); |
| } |
| } |
| |
| @Override |
| public void setAlpha(int alpha) { |
| |
| } |
| |
| @Override |
| public void setColorFilter(@Nullable ColorFilter colorFilter) { |
| |
| } |
| |
| @Override |
| public int getOpacity() { |
| return PixelFormat.OPAQUE; |
| } |
| |
| public void update(long dt) { |
| mDt = dt; |
| } |
| } |
| } |