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 107 setContentView(layout); 108 } 109 shouldWriteSettings()110 private boolean shouldWriteSettings() { 111 return getPackageName().equals("android"); 112 } 113 launchNextStage(boolean locked)114 private void launchNextStage(boolean locked) { 115 mClock.animate() 116 .alpha(0f).scaleX(0.5f).scaleY(0.5f) 117 .withEndAction(() -> mClock.setVisibility(View.GONE)) 118 .start(); 119 120 mLogo.setAlpha(0f); 121 mLogo.setScaleX(0.5f); 122 mLogo.setScaleY(0.5f); 123 mLogo.setVisibility(View.VISIBLE); 124 mLogo.animate() 125 .alpha(1f) 126 .scaleX(1f) 127 .scaleY(1f) 128 .setInterpolator(new OvershootInterpolator()) 129 .start(); 130 131 mLogo.postDelayed(() -> { 132 final ObjectAnimator anim = ObjectAnimator.ofInt(mBg, "level", 0, 10000); 133 anim.setInterpolator(new DecelerateInterpolator(1f)); 134 anim.start(); 135 }, 136 500 137 ); 138 139 final ContentResolver cr = getContentResolver(); 140 141 try { 142 if (shouldWriteSettings()) { 143 Log.v(TAG, "Saving egg unlock=" + locked); 144 syncTouchPressure(); 145 Settings.System.putLong(cr, 146 S_EGG_UNLOCK_SETTING, 147 locked ? 0 : System.currentTimeMillis()); 148 } 149 } catch (RuntimeException e) { 150 Log.e(TAG, "Can't write settings", e); 151 } 152 153 try { 154 startActivity(new Intent(Intent.ACTION_MAIN) 155 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 156 | Intent.FLAG_ACTIVITY_CLEAR_TASK) 157 .addCategory("com.android.internal.category.PLATLOGO")); 158 } catch (ActivityNotFoundException ex) { 159 Log.e("com.android.internal.app.PlatLogoActivity", "No more eggs."); 160 } 161 //finish(); // no longer finish upon unlock; it's fun to frob the dial 162 } 163 164 static final String TOUCH_STATS = "touch.stats"; 165 double mPressureMin = 0, mPressureMax = -1; 166 measureTouchPressure(MotionEvent event)167 private void measureTouchPressure(MotionEvent event) { 168 final float pressure = event.getPressure(); 169 switch (event.getActionMasked()) { 170 case MotionEvent.ACTION_DOWN: 171 if (mPressureMax < 0) { 172 mPressureMin = mPressureMax = pressure; 173 } 174 break; 175 case MotionEvent.ACTION_MOVE: 176 if (pressure < mPressureMin) mPressureMin = pressure; 177 if (pressure > mPressureMax) mPressureMax = pressure; 178 break; 179 } 180 } 181 syncTouchPressure()182 private void syncTouchPressure() { 183 try { 184 final String touchDataJson = Settings.System.getString( 185 getContentResolver(), TOUCH_STATS); 186 final JSONObject touchData = new JSONObject( 187 touchDataJson != null ? touchDataJson : "{}"); 188 if (touchData.has("min")) { 189 mPressureMin = Math.min(mPressureMin, touchData.getDouble("min")); 190 } 191 if (touchData.has("max")) { 192 mPressureMax = Math.max(mPressureMax, touchData.getDouble("max")); 193 } 194 if (mPressureMax >= 0) { 195 touchData.put("min", mPressureMin); 196 touchData.put("max", mPressureMax); 197 if (shouldWriteSettings()) { 198 Settings.System.putString(getContentResolver(), TOUCH_STATS, 199 touchData.toString()); 200 } 201 } 202 } catch (Exception e) { 203 Log.e("com.android.internal.app.PlatLogoActivity", "Can't write touch settings", e); 204 } 205 } 206 207 @Override onStart()208 public void onStart() { 209 super.onStart(); 210 syncTouchPressure(); 211 } 212 213 @Override onStop()214 public void onStop() { 215 syncTouchPressure(); 216 super.onStop(); 217 } 218 219 /** 220 * Subclass of AnalogClock that allows the user to flip up the glass and adjust the hands. 221 */ 222 public class SettableAnalogClock extends AnalogClock { 223 private int mOverrideHour = -1; 224 private int mOverrideMinute = -1; 225 private boolean mOverride = false; 226 SettableAnalogClock(Context context)227 public SettableAnalogClock(Context context) { 228 super(context); 229 } 230 231 @Override now()232 protected Instant now() { 233 final Instant realNow = super.now(); 234 final ZoneId tz = Clock.systemDefaultZone().getZone(); 235 final ZonedDateTime zdTime = realNow.atZone(tz); 236 if (mOverride) { 237 if (mOverrideHour < 0) { 238 mOverrideHour = zdTime.getHour(); 239 } 240 return Clock.fixed(zdTime 241 .withHour(mOverrideHour) 242 .withMinute(mOverrideMinute) 243 .withSecond(0) 244 .toInstant(), tz).instant(); 245 } else { 246 return realNow; 247 } 248 } 249 toPositiveDegrees(double rad)250 double toPositiveDegrees(double rad) { 251 return (Math.toDegrees(rad) + 360 - 90) % 360; 252 } 253 254 @Override onTouchEvent(MotionEvent ev)255 public boolean onTouchEvent(MotionEvent ev) { 256 switch (ev.getActionMasked()) { 257 case MotionEvent.ACTION_DOWN: 258 mOverride = true; 259 // pass through 260 case MotionEvent.ACTION_MOVE: 261 measureTouchPressure(ev); 262 263 float x = ev.getX(); 264 float y = ev.getY(); 265 float cx = getWidth() / 2f; 266 float cy = getHeight() / 2f; 267 float angle = (float) toPositiveDegrees(Math.atan2(x - cx, y - cy)); 268 269 int minutes = (75 - (int) (angle / 6)) % 60; 270 int minuteDelta = minutes - mOverrideMinute; 271 if (minuteDelta != 0) { 272 if (Math.abs(minuteDelta) > 45 && mOverrideHour >= 0) { 273 int hourDelta = (minuteDelta < 0) ? 1 : -1; 274 mOverrideHour = (mOverrideHour + 24 + hourDelta) % 24; 275 } 276 mOverrideMinute = minutes; 277 if (mOverrideMinute == 0) { 278 performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); 279 if (getScaleX() == 1f) { 280 setScaleX(1.05f); 281 setScaleY(1.05f); 282 animate().scaleX(1f).scaleY(1f).setDuration(150).start(); 283 } 284 } else { 285 performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK); 286 } 287 288 onTimeChanged(); 289 postInvalidate(); 290 } 291 292 return true; 293 case MotionEvent.ACTION_UP: 294 if (mOverrideMinute == 0 && (mOverrideHour % 12) == 0) { 295 Log.v(TAG, "12:00 let's gooooo"); 296 performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); 297 launchNextStage(false); 298 } 299 return true; 300 } 301 return false; 302 } 303 } 304 305 static class Bubble { 306 public float x, y, r; 307 public int color; 308 } 309 310 class BubblesDrawable extends Drawable { 311 private static final int MAX_BUBBS = 2000; 312 313 private final int[] mColorIds = { 314 android.R.color.system_accent1_400, 315 android.R.color.system_accent1_500, 316 android.R.color.system_accent1_600, 317 318 android.R.color.system_accent2_400, 319 android.R.color.system_accent2_500, 320 android.R.color.system_accent2_600, 321 }; 322 323 private int[] mColors = new int[mColorIds.length]; 324 325 private final Bubble[] mBubbs = new Bubble[MAX_BUBBS]; 326 private int mNumBubbs; 327 328 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 329 330 public float avoid = 0f; 331 public float padding = 0f; 332 public float minR = 0f; 333 BubblesDrawable()334 BubblesDrawable() { 335 for (int i = 0; i < mColorIds.length; i++) { 336 mColors[i] = getColor(mColorIds[i]); 337 } 338 for (int j = 0; j < mBubbs.length; j++) { 339 mBubbs[j] = new Bubble(); 340 } 341 } 342 343 @Override draw(Canvas canvas)344 public void draw(Canvas canvas) { 345 final float f = getLevel() / 10000f; 346 mPaint.setStyle(Paint.Style.FILL); 347 int drawn = 0; 348 for (int j = 0; j < mNumBubbs; j++) { 349 if (mBubbs[j].color == 0 || mBubbs[j].r == 0) continue; 350 mPaint.setColor(mBubbs[j].color); 351 canvas.drawCircle(mBubbs[j].x, mBubbs[j].y, mBubbs[j].r * f, mPaint); 352 drawn++; 353 } 354 } 355 356 @Override onLevelChange(int level)357 protected boolean onLevelChange(int level) { 358 invalidateSelf(); 359 return true; 360 } 361 362 @Override onBoundsChange(Rect bounds)363 protected void onBoundsChange(Rect bounds) { 364 super.onBoundsChange(bounds); 365 randomize(); 366 } 367 randomize()368 private void randomize() { 369 final float w = getBounds().width(); 370 final float h = getBounds().height(); 371 final float maxR = Math.min(w, h) / 3f; 372 mNumBubbs = 0; 373 if (avoid > 0f) { 374 mBubbs[mNumBubbs].x = w / 2f; 375 mBubbs[mNumBubbs].y = h / 2f; 376 mBubbs[mNumBubbs].r = avoid; 377 mBubbs[mNumBubbs].color = 0; 378 mNumBubbs++; 379 } 380 for (int j = 0; j < MAX_BUBBS; j++) { 381 // a simple but time-tested bubble-packing algorithm: 382 // 1. pick a spot 383 // 2. shrink the bubble until it is no longer overlapping any other bubble 384 // 3. if the bubble hasn't popped, keep it 385 int tries = 5; 386 while (tries-- > 0) { 387 float x = (float) Math.random() * w; 388 float y = (float) Math.random() * h; 389 float r = Math.min(Math.min(x, w - x), Math.min(y, h - y)); 390 391 // shrink radius to fit other bubbs 392 for (int i = 0; i < mNumBubbs; i++) { 393 r = (float) Math.min(r, 394 Math.hypot(x - mBubbs[i].x, y - mBubbs[i].y) - mBubbs[i].r 395 - padding); 396 if (r < minR) break; 397 } 398 399 if (r >= minR) { 400 // we have found a spot for this bubble to live, let's save it and move on 401 r = Math.min(maxR, r); 402 403 mBubbs[mNumBubbs].x = x; 404 mBubbs[mNumBubbs].y = y; 405 mBubbs[mNumBubbs].r = r; 406 mBubbs[mNumBubbs].color = mColors[(int) (Math.random() * mColors.length)]; 407 mNumBubbs++; 408 break; 409 } 410 } 411 } 412 Log.v(TAG, String.format("successfully placed %d bubbles (%d%%)", 413 mNumBubbs, (int) (100f * mNumBubbs / MAX_BUBBS))); 414 } 415 416 @Override setAlpha(int alpha)417 public void setAlpha(int alpha) { } 418 419 @Override setColorFilter(ColorFilter colorFilter)420 public void setColorFilter(ColorFilter colorFilter) { } 421 422 @Override getOpacity()423 public int getOpacity() { 424 return TRANSLUCENT; 425 } 426 } 427 428 } 429