blob: 71bbccb3d989aede572d4b87b83c503592e6e57f [file] [log] [blame]
/*
* 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;
}
}
}