• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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