EDIT: Even stranger. Setting the pixel format to translucent rather than opaque seems to have fixed it, i at least am unable to see the "stacked" numbers.
Very strange behavior.
I am using a service to draw a system_overlay view. The view adds and displays just fine.
This view is meant to be a countdown timer, so I need to update the text every second. I use a handler calling postDelayed to handle that, and call textView.setText("CONTENT")
from the runnable being executed by the handler.
This is where it gets weird.
The text updates, but seems to be stacking. I see 00:00 under the 00:01, etc etc. Each tick causes another layer.
I have tested this code and view in a standard activity and the text renders perfect, no "stacks". Its not until its being added via the WindowManager that the behavior is problematic.
TimerView.java
/*
* Copyright (C) 2013 The Android Open Source Project
*
* 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.t3hh4xx0r.lifelock.widgets;
import java.util.concurrent.TimeUnit;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PointF;
import android.os.Handler;
import android.os.IBinder;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.t3hh4xx0r.lifelock.R;
import com.t3hh4xx0r.lifelock.services.TimerDrawerService;
/**
* View used to draw a running timer.
*/
public class TimerView extends FrameLayout {
int alpha = 100;
TimerDrawerService.ServiceBinder drawerBinder;
PointF firstFinger;
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
firstFinger = new PointF(event.getX(), event.getY());
break;
case MotionEvent.ACTION_MOVE:
PointF newFinger = new PointF(event.getX(), event.getY());
float distance = newFinger.x - firstFinger.x;
float part = Math.abs(distance);
float percentOfMaxTraveled = (part * 100) / getWidth();
int nextAlpha = 100 - Float.valueOf(percentOfMaxTraveled).intValue();
if (nextAlpha < 20) {
Toast.makeText(getContext(), "Dismissed", Toast.LENGTH_LONG).show();
if (drawerBinder != null) {
drawerBinder.remove();
}
return true;
}
if (nextAlpha < alpha) {
alpha = nextAlpha;
}
Log.d("THE PERCENT TRAVELED", String.valueOf(percentOfMaxTraveled) + " : " + String.valueOf(alpha));
this.invalidate();
break;
}
return true;
}
@Override
public void onDraw(Canvas canvas) {
canvas.saveLayerAlpha(0, 0, canvas.getWidth(), canvas.getHeight(),
alpha, Canvas.HAS_ALPHA_LAYER_SAVE_FLAG);
super.onDraw(canvas);
}
/**
* Interface to listen for changes on the view layout.
*/
public interface ChangeListener {
/** Notified of a change in the view. */
public void onChange();
}
private static final long DELAY_MILLIS = 1000;
private final TextView mMinutesView;
private final TextView mSecondsView;
private final int mWhiteColor;
private final int mRedColor;
private final Handler mHandler = new Handler();
private final Runnable mUpdateTextRunnable = new Runnable() {
@Override
public void run() {
if (mRunning) {
mHandler.postDelayed(mUpdateTextRunnable, DELAY_MILLIS);
updateText();
}
}
};
private final Timer mTimer;
private final Timer.TimerListener mTimerListener = new Timer.TimerListener() {
@Override
public void onStart() {
mRunning = true;
long delayMillis = Math.abs(mTimer.getRemainingTimeMillis())
% DELAY_MILLIS;
if (delayMillis == 0) {
delayMillis = DELAY_MILLIS;
}
mHandler.postDelayed(mUpdateTextRunnable, delayMillis);
}
};
private boolean mRunning;
private boolean mRedText;
private ChangeListener mChangeListener;
public TimerView(Context context) {
this(context, null, 0);
}
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
if (service instanceof TimerDrawerService.ServiceBinder) {
drawerBinder = (com.t3hh4xx0r.lifelock.services.TimerDrawerService.ServiceBinder) service;
}
// No need to keep the service bound.
getContext().unbindService(this);
}
@Override
public void onServiceDisconnected(ComponentName name) {
// Nothing to do here.
}
};
public TimerView(Context context, AttributeSet attrs, int style) {
super(context, attrs, style);
context.bindService(new Intent(context, TimerDrawerService.class), mConnection, 0);
LayoutInflater.from(context).inflate(R.layout.timer, this);
mMinutesView = (TextView) findViewById(R.id.minutes);
mSecondsView = (TextView) findViewById(R.id.seconds);
mWhiteColor = context.getResources().getColor(android.R.color.white);
mRedColor = Color.RED;
mTimer = new Timer();
mTimer.setListener(mTimerListener);
mTimer.setDurationMillis(0);
}
public Timer getTimer() {
return mTimer;
}
/**
* Set a {@link ChangeListener}.
*/
public void setListener(ChangeListener listener) {
mChangeListener = listener;
}
/**
* Updates the text from the Timer's value.
*/
private void updateText() {
long remainingTimeMillis = mTimer.getRemainingTimeMillis();
if (remainingTimeMillis > 0) {
mRedText = false;
// Round up: x001 to (x + 1)000 milliseconds should resolve to x
// seconds.
remainingTimeMillis -= 1;
remainingTimeMillis += TimeUnit.SECONDS.toMillis(1);
} else {
mRedText = !mRedText;
remainingTimeMillis = Math.abs(remainingTimeMillis);
}
if (mRedText) {
// Sync the sound with the red text.
}
updateText(remainingTimeMillis, mRedText ? mRedColor : mWhiteColor);
}
/**
* Updates the displayed text with the provided values.
*/
private void updateText(long timeMillis, int textColor) {
timeMillis %= TimeUnit.HOURS.toMillis(1);
mMinutesView.setText(String.format("%02d",
TimeUnit.MILLISECONDS.toMinutes(timeMillis)));
mMinutesView.setTextColor(textColor);
timeMillis %= TimeUnit.MINUTES.toMillis(1);
mSecondsView.setText(String.format("%02d",
TimeUnit.MILLISECONDS.toSeconds(timeMillis)));
mSecondsView.setTextColor(textColor);
if (mChangeListener != null) {
mChangeListener.onChange();
}
}
public void showMessage(boolean didGood) {
// mTipView.setText((didGood ? "Good" : "Bad") + " job!");
}
public void setLocked(boolean b) {
if (b) {
((WindowManager.LayoutParams) getLayoutParams()).type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;
} else {
((WindowManager.LayoutParams) getLayoutParams()).type = WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY;
}
}
}
TimerDrawerService
package com.t3hh4xx0r.lifelock.services;
/*
Copyright 2011 jawsware international
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.
*/
import android.app.ActivityManager;
import android.app.ActivityManager.RunningServiceInfo;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.graphics.PixelFormat;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;
import android.view.Gravity;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import com.t3hh4xx0r.lifelock.objects.Peek;
import com.t3hh4xx0r.lifelock.widgets.TimerView;
public class TimerDrawerService extends Service {
TimerView root;
Peek currentInstance;
private ServiceBinder mBinder = new ServiceBinder();
public class ServiceBinder extends Binder {
public TimerView getRoot() {
return root;
}
public void remove() {
removeViews();
}
public void add() {
addViews();
}
}
static public void start(Context c, Peek currentInstance) {
Intent i = new Intent(c, TimerDrawerService.class);
i.putExtra("peek", currentInstance);
c.startService(i);
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
@Override
public void onCreate() {
super.onCreate();
Log.d("CRATING VIEW HERE!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!",
"NOW MAN");
root = new TimerView(this);
root.getTimer().setDurationMillis(90 * 1000);
root.getTimer().start();
addViews();
}
public void addViews() {
((WindowManager) getSystemService(Context.WINDOW_SERVICE)).addView(
root, getLayoutParams());
}
public static boolean isRunning(Context c) {
ActivityManager manager = (ActivityManager) c
.getSystemService(ACTIVITY_SERVICE);
for (RunningServiceInfo service : manager
.getRunningServices(Integer.MAX_VALUE)) {
if ("com.t3hh4xx0r.lifelock.service.TimerDrawerService"
.equals(service.service.getClassName())) {
return true;
}
}
return false;
}
private WindowManager.LayoutParams getLayoutParams() {
LayoutParams layoutParams = new WindowManager.LayoutParams(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.TYPE_SYSTEM_ERROR, 0,
PixelFormat.OPAQUE);
layoutParams.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
layoutParams.gravity = Gravity.CENTER;
return layoutParams;
}
@Override
public void onDestroy() {
super.onDestroy();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
currentInstance = (Peek) intent.getSerializableExtra("peek");
return START_STICKY;
}
public void removeViews() {
((WindowManager) getSystemService(Context.WINDOW_SERVICE))
.removeView(root);
}
}