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