/*
* Copyright 2013 Google Inc.
*
* 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.google.android.apps.dashclock;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.annotation.TargetApi;
import android.content.ComponentName;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Color;
import android.os.Build;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.service.dreams.DreamService;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Property;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.animation.LinearInterpolator;
import android.widget.FrameLayout;
import android.widget.ScrollView;
import com.google.android.apps.dashclock.configuration.AppearanceConfig;
import com.google.android.apps.dashclock.render.DashClockRenderer;
import com.google.android.apps.dashclock.render.SimpleRenderer;
import com.google.android.apps.dashclock.render.SimpleViewBuilder;
import net.nurik.roman.dashclock.R;
import java.util.List;
import static com.google.android.apps.dashclock.ExtensionManager.ExtensionWithData;
import static com.google.android.apps.dashclock.Utils.SECONDS_MILLIS;
/**
* Daydream for DashClock.
*/
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
public class DaydreamService extends DreamService implements
ExtensionManager.OnChangeListener,
DashClockRenderer.OnClickListener {
public static final String PREF_DAYDREAM_COLOR = "pref_daydream_color";
public static final String PREF_DAYDREAM_NIGHT_MODE = "pref_daydream_night_mode";
public static final String PREF_DAYDREAM_ANIMATION = "pref_daydream_animation";
private static final int ANIMATION_HAS_ROTATE = 0x1;
private static final int ANIMATION_HAS_SLIDE = 0x2;
private static final int ANIMATION_HAS_FADE = 0x4;
private static final int ANIMATION_NONE = 0;
private static final int ANIMATION_FADE = ANIMATION_HAS_FADE;
private static final int ANIMATION_SLIDE = ANIMATION_FADE | ANIMATION_HAS_SLIDE;
private static final int ANIMATION_PENDULUM = ANIMATION_SLIDE | ANIMATION_HAS_ROTATE;
private static final int CYCLE_INTERVAL_MILLIS = 20 * SECONDS_MILLIS;
private static final int FADE_MILLIS = 5 * SECONDS_MILLIS;
private static final int TRAVEL_ROTATE_DEGREES = 3;
private static final float SCALE_WHEN_MOVING = 0.85f;
private Handler mHandler = new Handler();
private ExtensionManager mExtensionManager;
private int mTravelDistance;
private int mForegroundColor;
private int mAnimation;
private ViewGroup mDaydreamContainer;
private ViewGroup mExtensionsContainer;
private AnimatorSet mSingleCycleAnimator;
private boolean mAttached;
private boolean mNeedsRelayout;
private boolean mMovingLeft;
private boolean mManuallyAwoken;
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
mExtensionManager = ExtensionManager.getInstance(this);
mExtensionManager.addOnChangeListener(this);
// Update extensions and ensure the periodic refresh is set up.
PeriodicExtensionRefreshReceiver.updateExtensionsAndEnsurePeriodicRefresh(this);
mAttached = true;
setInteractive(true);
setFullscreen(true);
// Read preferences
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
mForegroundColor = sp.getInt(PREF_DAYDREAM_COLOR,
AppearanceConfig.DEFAULT_WIDGET_FOREGROUND_COLOR);
String animation = sp.getString(PREF_DAYDREAM_ANIMATION, "");
if ("none".equals(animation)) {
mAnimation = ANIMATION_NONE;
} else if ("slide".equals(animation)) {
mAnimation = ANIMATION_SLIDE;
} else if ("fade".equals(animation)) {
mAnimation = ANIMATION_FADE;
} else {
mAnimation = ANIMATION_PENDULUM;
}
setScreenBright(!sp.getBoolean(PREF_DAYDREAM_NIGHT_MODE, true));
// Begin daydream
layoutDream();
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
mExtensionManager.removeOnChangeListener(this);
mExtensionManager = null;
mHandler.removeCallbacksAndMessages(null);
mAttached = false;
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mHandler.removeCallbacks(mCycleRunnable);
layoutDream();
}
@Override
public void onExtensionsChanged(ComponentName sourceExtension) {
mHandler.removeCallbacks(mHandleExtensionsChanged);
mHandler.postDelayed(mHandleExtensionsChanged,
DashClockService.UPDATE_COLLAPSE_TIME_MILLIS);
}
private Runnable mHandleExtensionsChanged = new Runnable() {
@Override
public void run() {
renderDaydream(false);
}
};
private void layoutDream() {
setContentView(R.layout.daydream);
mNeedsRelayout = true;
renderDaydream(true);
mHandler.removeCallbacks(mCycleRunnable);
mHandler.postDelayed(mCycleRunnable, CYCLE_INTERVAL_MILLIS - FADE_MILLIS);
}
private void renderDaydream(final boolean restartAnimation) {
if (!mAttached || mExtensionManager == null) {
return;
}
if (restartAnimation) {
// Only modify fullscreen state if this render will restart an animation (enter a new
// cycle)
setFullscreen(true);
}
final Resources res = getResources();
mDaydreamContainer = (ViewGroup) findViewById(R.id.daydream_container);
RootLayout rootContainer = (RootLayout)
findViewById(R.id.daydream_root);
if (mTravelDistance == 0) {
mTravelDistance = rootContainer.getWidth() / 4;
}
rootContainer.setRootLayoutListener(new RootLayout.RootLayoutListener() {
@Override
public void onAwake() {
mManuallyAwoken = true;
setFullscreen(false);
mHandler.removeCallbacks(mCycleRunnable);
mHandler.postDelayed(mCycleRunnable, CYCLE_INTERVAL_MILLIS);
mDaydreamContainer.animate()
.alpha(1f)
.rotation(0)
.scaleX(1f)
.scaleY(1f)
.translationX(0f)
.translationY(0f)
.setDuration(res.getInteger(android.R.integer.config_shortAnimTime));
if (mSingleCycleAnimator != null) {
mSingleCycleAnimator.cancel();
}
}
@Override
public boolean isAwake() {
return mManuallyAwoken;
}
@Override
public void onSizeChanged(int width, int height) {
mTravelDistance = width / 4;
}
});
DisplayMetrics displayMetrics = res.getDisplayMetrics();
int screenWidthDp = (int) (displayMetrics.widthPixels * 1f / displayMetrics.density);
int screenHeightDp = (int) (displayMetrics.heightPixels * 1f / displayMetrics.density);
// Set up rendering
SimpleRenderer renderer = new SimpleRenderer(this);
DashClockRenderer.Options options = new DashClockRenderer.Options();
options.target = DashClockRenderer.Options.TARGET_DAYDREAM;
options.foregroundColor = Color.WHITE;
options.minWidthDp = screenWidthDp;
options.minHeightDp = screenHeightDp;
options.newTaskOnClick = true;
options.onClickListener = this;
options.clickIntentTemplate = WidgetClickProxyActivity.getTemplate(this);
renderer.setOptions(options);
// Render the clock face
SimpleViewBuilder vb = renderer.createSimpleViewBuilder();
vb.useRoot(mDaydreamContainer);
renderer.renderClockFace(vb, options.foregroundColor);
vb.setLinearLayoutGravity(R.id.clock_target, Gravity.CENTER_HORIZONTAL);
// Render extensions
mExtensionsContainer = (ViewGroup) findViewById(R.id.extensions_container);
mExtensionsContainer.removeAllViews();
List<ExtensionWithData> visibleExtensions
= mExtensionManager.getVisibleExtensionsWithData();
for (ExtensionWithData ewd : visibleExtensions) {
mExtensionsContainer.addView(
(View) renderer.renderExpandedExtension(mExtensionsContainer, null, false,
ewd));
}
if (mDaydreamContainer.getHeight() == 0 || mNeedsRelayout) {
ViewTreeObserver vto = mDaydreamContainer.getViewTreeObserver();
if (vto.isAlive()) {
vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
ViewTreeObserver vto = mDaydreamContainer.getViewTreeObserver();
if (vto.isAlive()) {
vto.removeOnGlobalLayoutListener(this);
}
postLayoutRender(restartAnimation);
}
});
}
mDaydreamContainer.requestLayout();
mNeedsRelayout = false;
} else {
postLayoutRender(restartAnimation);
}
}
/**
* Post-layout render code.
*/
public void postLayoutRender(boolean restartAnimation) {
// Adjust the ScrollView
ExposedScrollView scrollView = (ExposedScrollView) findViewById(R.id.extensions_scroller);
int maxScroll = scrollView.computeVerticalScrollRange() - scrollView.getHeight();
if (maxScroll < 0) {
ViewGroup.LayoutParams lp = scrollView.getLayoutParams();
lp.height = mExtensionsContainer.getHeight();
scrollView.setLayoutParams(lp);
mDaydreamContainer.requestLayout();
}
// Recolor widget
Utils.traverseAndRecolor(mDaydreamContainer, mForegroundColor, true, true);
if (restartAnimation) {
int x = 0;
int deg = 0;
if ((mAnimation & ANIMATION_HAS_SLIDE) != 0) {
x = (mMovingLeft ? 1 : -1) * mTravelDistance;
}
if ((mAnimation & ANIMATION_HAS_ROTATE) != 0) {
deg = (mMovingLeft ? 1 : -1) * TRAVEL_ROTATE_DEGREES;
}
mMovingLeft = !mMovingLeft;
mDaydreamContainer.animate().cancel();
if ((mAnimation & ANIMATION_HAS_SLIDE) != 0) {
// Only use small size when moving
mDaydreamContainer.setScaleX(SCALE_WHEN_MOVING);
mDaydreamContainer.setScaleY(SCALE_WHEN_MOVING);
}
if (mSingleCycleAnimator != null) {
mSingleCycleAnimator.cancel();
}
Animator scrollDownAnimator = ObjectAnimator.ofInt(scrollView,
ExposedScrollView.SCROLL_POS, 0, maxScroll);
scrollDownAnimator.setDuration(CYCLE_INTERVAL_MILLIS / 5);
scrollDownAnimator.setStartDelay(CYCLE_INTERVAL_MILLIS / 5);
Animator scrollUpAnimator = ObjectAnimator.ofInt(scrollView,
ExposedScrollView.SCROLL_POS, 0);
scrollUpAnimator.setDuration(CYCLE_INTERVAL_MILLIS / 5);
scrollUpAnimator.setStartDelay(CYCLE_INTERVAL_MILLIS / 5);
AnimatorSet scrollAnimator = new AnimatorSet();
scrollAnimator.playSequentially(scrollDownAnimator, scrollUpAnimator);
Animator moveAnimator = ObjectAnimator.ofFloat(mDaydreamContainer,
View.TRANSLATION_X, x, -x).setDuration(CYCLE_INTERVAL_MILLIS);
moveAnimator.setInterpolator(new LinearInterpolator());
Animator rotateAnimator = ObjectAnimator.ofFloat(mDaydreamContainer,
View.ROTATION, deg, -deg).setDuration(CYCLE_INTERVAL_MILLIS);
moveAnimator.setInterpolator(new LinearInterpolator());
mSingleCycleAnimator = new AnimatorSet();
mSingleCycleAnimator.playTogether(scrollAnimator, moveAnimator, rotateAnimator);
mSingleCycleAnimator.start();
}
}
public Runnable mCycleRunnable = new Runnable() {
@Override
public void run() {
mManuallyAwoken = false;
float outAlpha = 1f;
if ((mAnimation & ANIMATION_HAS_FADE) != 0) {
outAlpha = 0f;
}
mDaydreamContainer.animate().alpha(outAlpha).setDuration(FADE_MILLIS)
.withEndAction(new Runnable() {
@Override
public void run() {
renderDaydream(true);
mHandler.removeCallbacks(mCycleRunnable);
mHandler.postDelayed(mCycleRunnable,
CYCLE_INTERVAL_MILLIS - FADE_MILLIS);
mDaydreamContainer.animate().alpha(1f).setDuration(FADE_MILLIS);
}
});
}
};
@Override
public void onClick() {
// Any time anything in DashClock is clicked
finish();
}
/**
* FrameLayout that can notify listeners of ACTION_DOWN events.
*/
public static class RootLayout extends FrameLayout {
private RootLayoutListener mRootLayoutListener;
private boolean mCancelCurrentEvent;
public RootLayout(Context context) {
super(context);
}
public RootLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public RootLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getActionMasked()) {
// ACTION_UP doesn't seem to reliably get called. Otherwise
// should postDelayed on ACTION_UP instead of ACTION_DOWN.
case MotionEvent.ACTION_DOWN:
if (mRootLayoutListener != null && !mRootLayoutListener.isAwake()) {
mCancelCurrentEvent = true;
mRootLayoutListener.onAwake();
} else {
mCancelCurrentEvent = false;
}
break;
}
return mCancelCurrentEvent;
}
public void setRootLayoutListener(RootLayoutListener rootLayoutListener) {
mRootLayoutListener = rootLayoutListener;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (mRootLayoutListener != null) {
mRootLayoutListener.onSizeChanged(w, h);
}
}
public static interface RootLayoutListener {
void onAwake();
void onSizeChanged(int width, int height);
boolean isAwake();
}
}
/**
* ScrollView that exposes its scroll range.
*/
public static class ExposedScrollView extends ScrollView {
public ExposedScrollView(Context context) {
super(context);
}
public ExposedScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ExposedScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public int computeVerticalScrollRange() {
return super.computeVerticalScrollRange();
}
public static final Property<ScrollView, Integer> SCROLL_POS
= new Property<ScrollView, Integer>(Integer.class, "scrollPos") {
@Override
public void set(ScrollView object, Integer value) {
object.scrollTo(0, value);
}
@Override
public Integer get(ScrollView object) {
return object.getScrollY();
}
};
}
}