• 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.graphics.PixelFormat.TRANSLUCENT;
20 
21 import android.animation.ObjectAnimator;
22 import android.app.ActionBar;
23 import android.app.Activity;
24 import android.content.ActivityNotFoundException;
25 import android.content.ContentResolver;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.graphics.Canvas;
29 import android.graphics.ColorFilter;
30 import android.graphics.Paint;
31 import android.graphics.Rect;
32 import android.graphics.drawable.Drawable;
33 import android.os.Bundle;
34 import android.provider.Settings;
35 import android.util.DisplayMetrics;
36 import android.util.Log;
37 import android.view.Gravity;
38 import android.view.HapticFeedbackConstants;
39 import android.view.MotionEvent;
40 import android.view.View;
41 import android.view.animation.DecelerateInterpolator;
42 import android.view.animation.OvershootInterpolator;
43 import android.widget.AnalogClock;
44 import android.widget.FrameLayout;
45 import android.widget.ImageView;
46 
47 import com.android.internal.R;
48 
49 import org.json.JSONObject;
50 
51 import java.time.Clock;
52 import java.time.Instant;
53 import java.time.ZoneId;
54 import java.time.ZonedDateTime;
55 
56 /**
57  * @hide
58  */
59 public class PlatLogoActivity extends Activity {
60     private static final String TAG = "PlatLogoActivity";
61 
62     private static final String S_EGG_UNLOCK_SETTING = "egg_mode_s";
63 
64     private SettableAnalogClock mClock;
65     private ImageView mLogo;
66     private BubblesDrawable mBg;
67 
68     @Override
onPause()69     protected void onPause() {
70         super.onPause();
71     }
72 
73     @Override
onCreate(Bundle savedInstanceState)74     protected void onCreate(Bundle savedInstanceState) {
75         super.onCreate(savedInstanceState);
76 
77         getWindow().setNavigationBarColor(0);
78         getWindow().setStatusBarColor(0);
79 
80         final ActionBar ab = getActionBar();
81         if (ab != null) ab.hide();
82 
83         final FrameLayout layout = new FrameLayout(this);
84 
85         mClock = new SettableAnalogClock(this);
86 
87         final DisplayMetrics dm = getResources().getDisplayMetrics();
88         final float dp = dm.density;
89         final int minSide = Math.min(dm.widthPixels, dm.heightPixels);
90         final int widgetSize = (int) (minSide * 0.75);
91         final FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(widgetSize, widgetSize);
92         lp.gravity = Gravity.CENTER;
93         layout.addView(mClock, lp);
94 
95         mLogo = new ImageView(this);
96         mLogo.setVisibility(View.GONE);
97         mLogo.setImageResource(R.drawable.platlogo);
98         layout.addView(mLogo, lp);
99 
100         mBg = new BubblesDrawable();
101         mBg.setLevel(0);
102         mBg.avoid = widgetSize / 2;
103         mBg.padding = 0.5f * dp;
104         mBg.minR = 1 * dp;
105         layout.setBackground(mBg);
106         layout.setOnLongClickListener(mBg);
107 
108         setContentView(layout);
109     }
110 
shouldWriteSettings()111     private boolean shouldWriteSettings() {
112         return getPackageName().equals("android");
113     }
114 
launchNextStage(boolean locked)115     private void launchNextStage(boolean locked) {
116         mClock.animate()
117                 .alpha(0f).scaleX(0.5f).scaleY(0.5f)
118                 .withEndAction(() -> mClock.setVisibility(View.GONE))
119                 .start();
120 
121         mLogo.setAlpha(0f);
122         mLogo.setScaleX(0.5f);
123         mLogo.setScaleY(0.5f);
124         mLogo.setVisibility(View.VISIBLE);
125         mLogo.animate()
126                 .alpha(1f)
127                 .scaleX(1f)
128                 .scaleY(1f)
129                 .setInterpolator(new OvershootInterpolator())
130                 .start();
131 
132         mLogo.postDelayed(() -> {
133                     final ObjectAnimator anim = ObjectAnimator.ofInt(mBg, "level", 0, 10000);
134                     anim.setInterpolator(new DecelerateInterpolator(1f));
135                     anim.start();
136                 },
137                 500
138         );
139 
140         final ContentResolver cr = getContentResolver();
141 
142         try {
143             if (shouldWriteSettings()) {
144                 Log.v(TAG, "Saving egg unlock=" + locked);
145                 syncTouchPressure();
146                 Settings.System.putLong(cr,
147                         S_EGG_UNLOCK_SETTING,
148                         locked ? 0 : System.currentTimeMillis());
149             }
150         } catch (RuntimeException e) {
151             Log.e(TAG, "Can't write settings", e);
152         }
153 
154         try {
155             startActivity(new Intent(Intent.ACTION_MAIN)
156                     .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
157                             | Intent.FLAG_ACTIVITY_CLEAR_TASK)
158                     .addCategory("com.android.internal.category.PLATLOGO"));
159         } catch (ActivityNotFoundException ex) {
160             Log.e("com.android.internal.app.PlatLogoActivity", "No more eggs.");
161         }
162         //finish(); // no longer finish upon unlock; it's fun to frob the dial
163     }
164 
165     static final String TOUCH_STATS = "touch.stats";
166     double mPressureMin = 0, mPressureMax = -1;
167 
measureTouchPressure(MotionEvent event)168     private void measureTouchPressure(MotionEvent event) {
169         final float pressure = event.getPressure();
170         switch (event.getActionMasked()) {
171             case MotionEvent.ACTION_DOWN:
172                 if (mPressureMax < 0) {
173                     mPressureMin = mPressureMax = pressure;
174                 }
175                 break;
176             case MotionEvent.ACTION_MOVE:
177                 if (pressure < mPressureMin) mPressureMin = pressure;
178                 if (pressure > mPressureMax) mPressureMax = pressure;
179                 break;
180         }
181     }
182 
syncTouchPressure()183     private void syncTouchPressure() {
184         try {
185             final String touchDataJson = Settings.System.getString(
186                     getContentResolver(), TOUCH_STATS);
187             final JSONObject touchData = new JSONObject(
188                     touchDataJson != null ? touchDataJson : "{}");
189             if (touchData.has("min")) {
190                 mPressureMin = Math.min(mPressureMin, touchData.getDouble("min"));
191             }
192             if (touchData.has("max")) {
193                 mPressureMax = Math.max(mPressureMax, touchData.getDouble("max"));
194             }
195             if (mPressureMax >= 0) {
196                 touchData.put("min", mPressureMin);
197                 touchData.put("max", mPressureMax);
198                 if (shouldWriteSettings()) {
199                     Settings.System.putString(getContentResolver(), TOUCH_STATS,
200                             touchData.toString());
201                 }
202             }
203         } catch (Exception e) {
204             Log.e("com.android.internal.app.PlatLogoActivity", "Can't write touch settings", e);
205         }
206     }
207 
208     @Override
onStart()209     public void onStart() {
210         super.onStart();
211         syncTouchPressure();
212     }
213 
214     @Override
onStop()215     public void onStop() {
216         syncTouchPressure();
217         super.onStop();
218     }
219 
220     /**
221      * Subclass of AnalogClock that allows the user to flip up the glass and adjust the hands.
222      */
223     public class SettableAnalogClock extends AnalogClock {
224         private int mOverrideHour = -1;
225         private int mOverrideMinute = -1;
226         private boolean mOverride = false;
227 
SettableAnalogClock(Context context)228         public SettableAnalogClock(Context context) {
229             super(context);
230         }
231 
232         @Override
now()233         protected Instant now() {
234             final Instant realNow = super.now();
235             final ZoneId tz = Clock.systemDefaultZone().getZone();
236             final ZonedDateTime zdTime = realNow.atZone(tz);
237             if (mOverride) {
238                 if (mOverrideHour < 0) {
239                     mOverrideHour = zdTime.getHour();
240                 }
241                 return Clock.fixed(zdTime
242                         .withHour(mOverrideHour)
243                         .withMinute(mOverrideMinute)
244                         .withSecond(0)
245                         .toInstant(), tz).instant();
246             } else {
247                 return realNow;
248             }
249         }
250 
toPositiveDegrees(double rad)251         double toPositiveDegrees(double rad) {
252             return (Math.toDegrees(rad) + 360 - 90) % 360;
253         }
254 
255         @Override
onTouchEvent(MotionEvent ev)256         public boolean onTouchEvent(MotionEvent ev) {
257             switch (ev.getActionMasked()) {
258                 case MotionEvent.ACTION_DOWN:
259                     mOverride = true;
260                     // pass through
261                 case MotionEvent.ACTION_MOVE:
262                     measureTouchPressure(ev);
263 
264                     float x = ev.getX();
265                     float y = ev.getY();
266                     float cx = getWidth() / 2f;
267                     float cy = getHeight() / 2f;
268                     float angle = (float) toPositiveDegrees(Math.atan2(x - cx, y - cy));
269 
270                     int minutes = (75 - (int) (angle / 6)) % 60;
271                     int minuteDelta = minutes - mOverrideMinute;
272                     if (minuteDelta != 0) {
273                         if (Math.abs(minuteDelta) > 45 && mOverrideHour >= 0) {
274                             int hourDelta = (minuteDelta < 0) ? 1 : -1;
275                             mOverrideHour = (mOverrideHour + 24 + hourDelta) % 24;
276                         }
277                         mOverrideMinute = minutes;
278                         if (mOverrideMinute == 0) {
279                             performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
280                             if (getScaleX() == 1f) {
281                                 setScaleX(1.05f);
282                                 setScaleY(1.05f);
283                                 animate().scaleX(1f).scaleY(1f).setDuration(150).start();
284                             }
285                         } else {
286                             performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
287                         }
288 
289                         onTimeChanged();
290                         postInvalidate();
291                     }
292 
293                     return true;
294                 case MotionEvent.ACTION_UP:
295                     if (mOverrideMinute == 0 && (mOverrideHour % 12) == 1) {
296                         Log.v(TAG, "13:00");
297                         performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
298                         launchNextStage(false);
299                     }
300                     return true;
301             }
302             return false;
303         }
304     }
305 
306     private static final String[][] EMOJI_SETS = {
307             {"��", "��", "��", "��", "��", "��", "��", "��", "��", "��", "��", "��",
308                     "��", "��", "��", "��"},
309             {"��", "��", "��", "��", "��", "��", "��", "��", "��"},
310             {"��", "��", "��", "��", "��", "��", "��", "��", "��", "��", "��", "��", "��",
311                     "��", "��", "��", "��", "��", "��", "☺️", "��", "��", "��", "��", "��", "��",
312                     "��", "��", "��", "��", "��", "��", "��", "��", "��", "��", "��", "��", "��",
313                     "��", "��", "��", "��", "��", "��", "��", "��", "��", "��", "��", "��", "��",
314                     "��"},
315             { "��", "��", "��", "��", "��", "��", "��" },
316             { "��" },
317             {"��", "��", "��", "��", "��", "��", "��", "❣", "��", "❤", "��", "��",
318                     "��", "��", "��", "��", "��", "��"},
319             // {"��", "️��", "��️"}, // this one is too much
320             {"��", "��", "✨", "��", "��", "��", "��", "��", "⭐", "��"},
321             {"��", "��", "��", "��", "��", "��", "��", "��"},
322             {"��", "��", "��", "��", "��", "��", "��", "��", "��", "��", "��", "��", "��", "��",
323                     "��"},
324             {"��", "��", "��", "��", "��"},
325             {"♈", "♉", "♊", "♋", "♌", "♍", "♎", "♏", "♐", "♑", "♒", "♓"},
326             {"��", "��", "��", "��", "��", "��", "��", "��", "��", "��", "��", "��", "��", "��",
327                     "��", "��", "��", "��", "��", "��", "��", "��", "��", "��"},
328             {"��", "��", "��", "��️", "��", "��"},
329             {"��", "✨", "��", "��"}
330     };
331 
332     static class Bubble {
333         public float x, y, r;
334         public int color;
335         public String text = null;
336     }
337 
338     class BubblesDrawable extends Drawable implements View.OnLongClickListener {
339         private static final int MAX_BUBBS = 2000;
340 
341         private final int[] mColorIds = {
342                 android.R.color.system_accent3_400,
343                 android.R.color.system_accent3_500,
344                 android.R.color.system_accent3_600,
345 
346                 android.R.color.system_accent2_400,
347                 android.R.color.system_accent2_500,
348                 android.R.color.system_accent2_600,
349         };
350 
351         private int[] mColors = new int[mColorIds.length];
352 
353         private int mEmojiSet = -1;
354 
355         private final Bubble[] mBubbs = new Bubble[MAX_BUBBS];
356         private int mNumBubbs;
357 
358         private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
359 
360         public float avoid = 0f;
361         public float padding = 0f;
362         public float minR = 0f;
363 
BubblesDrawable()364         BubblesDrawable() {
365             for (int i = 0; i < mColorIds.length; i++) {
366                 mColors[i] = getColor(mColorIds[i]);
367             }
368             for (int j = 0; j < mBubbs.length; j++) {
369                 mBubbs[j] = new Bubble();
370             }
371         }
372 
373         @Override
draw(Canvas canvas)374         public void draw(Canvas canvas) {
375             if (getLevel() == 0) return;
376             final float f = getLevel() / 10000f;
377             mPaint.setStyle(Paint.Style.FILL);
378             mPaint.setTextAlign(Paint.Align.CENTER);
379             int drawn = 0;
380             for (int j = 0; j < mNumBubbs; j++) {
381                 if (mBubbs[j].color == 0 || mBubbs[j].r == 0) continue;
382                 if (mBubbs[j].text != null) {
383                     mPaint.setTextSize(mBubbs[j].r * 1.75f);
384                     canvas.drawText(mBubbs[j].text, mBubbs[j].x,
385                             mBubbs[j].y  + mBubbs[j].r * f * 0.6f, mPaint);
386                 } else {
387                     mPaint.setColor(mBubbs[j].color);
388                     canvas.drawCircle(mBubbs[j].x, mBubbs[j].y, mBubbs[j].r * f, mPaint);
389                 }
390                 drawn++;
391             }
392         }
393 
chooseEmojiSet()394         public void chooseEmojiSet() {
395             mEmojiSet = (int) (Math.random() * EMOJI_SETS.length);
396             final String[] emojiSet = EMOJI_SETS[mEmojiSet];
397             for (int j = 0; j < mBubbs.length; j++) {
398                 mBubbs[j].text = emojiSet[(int) (Math.random() * emojiSet.length)];
399             }
400             invalidateSelf();
401         }
402 
403         @Override
onLevelChange(int level)404         protected boolean onLevelChange(int level) {
405             invalidateSelf();
406             return true;
407         }
408 
409         @Override
onBoundsChange(Rect bounds)410         protected void onBoundsChange(Rect bounds) {
411             super.onBoundsChange(bounds);
412             randomize();
413         }
414 
randomize()415         private void randomize() {
416             final float w = getBounds().width();
417             final float h = getBounds().height();
418             final float maxR = Math.min(w, h) / 3f;
419             mNumBubbs = 0;
420             if (avoid > 0f) {
421                 mBubbs[mNumBubbs].x = w / 2f;
422                 mBubbs[mNumBubbs].y = h / 2f;
423                 mBubbs[mNumBubbs].r = avoid;
424                 mBubbs[mNumBubbs].color = 0;
425                 mNumBubbs++;
426             }
427             for (int j = 0; j < MAX_BUBBS; j++) {
428                 // a simple but time-tested bubble-packing algorithm:
429                 // 1. pick a spot
430                 // 2. shrink the bubble until it is no longer overlapping any other bubble
431                 // 3. if the bubble hasn't popped, keep it
432                 int tries = 5;
433                 while (tries-- > 0) {
434                     float x = (float) Math.random() * w;
435                     float y = (float) Math.random() * h;
436                     float r = Math.min(Math.min(x, w - x), Math.min(y, h - y));
437 
438                     // shrink radius to fit other bubbs
439                     for (int i = 0; i < mNumBubbs; i++) {
440                         r = (float) Math.min(r,
441                                 Math.hypot(x - mBubbs[i].x, y - mBubbs[i].y) - mBubbs[i].r
442                                         - padding);
443                         if (r < minR) break;
444                     }
445 
446                     if (r >= minR) {
447                         // we have found a spot for this bubble to live, let's save it and move on
448                         r = Math.min(maxR, r);
449 
450                         mBubbs[mNumBubbs].x = x;
451                         mBubbs[mNumBubbs].y = y;
452                         mBubbs[mNumBubbs].r = r;
453                         mBubbs[mNumBubbs].color = mColors[(int) (Math.random() * mColors.length)];
454                         mNumBubbs++;
455                         break;
456                     }
457                 }
458             }
459             Log.v(TAG, String.format("successfully placed %d bubbles (%d%%)",
460                     mNumBubbs, (int) (100f * mNumBubbs / MAX_BUBBS)));
461         }
462 
463         @Override
setAlpha(int alpha)464         public void setAlpha(int alpha) { }
465 
466         @Override
setColorFilter(ColorFilter colorFilter)467         public void setColorFilter(ColorFilter colorFilter) { }
468 
469         @Override
getOpacity()470         public int getOpacity() {
471             return TRANSLUCENT;
472         }
473 
474         @Override
onLongClick(View v)475         public boolean onLongClick(View v) {
476             if (getLevel() == 0) return false;
477             chooseEmojiSet();
478             return true;
479         }
480     }
481 
482 }
483