/*
* Copyright (C) 2014 AChep@xda <artemchep@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package com.achep.acdisplay.services.activemode.sensors;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.util.Log;
import com.achep.acdisplay.Config;
import com.achep.acdisplay.services.activemode.ActiveModeSensor;
import com.achep.base.content.ConfigBase;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import static com.achep.base.Build.DEBUG;
/**
* Basing on results of proximity sensor it notifies when
* {@link com.achep.acdisplay.ui.activities.AcDisplayActivity AcDisplay}
* should be shown.
*
* @author Artem Chepurnoy
*/
public final class ProximitySensor extends ActiveModeSensor implements
SensorEventListener, ConfigBase.OnConfigChangedListener {
private static final String TAG = "ProximitySensor";
private static final int LAST_EVENT_MAX_TIME = 1000; // ms.
// pocket program
private static final int POCKET_START_DELAY = 4000; // ms.
private static WeakReference<ProximitySensor> sProximitySensorWeak;
private static long sLastEventTime;
private static boolean sAttached;
private static boolean sNear;
private float mMaximumRange;
private boolean mFirstChange;
@NonNull
private final Object monitor = new Object();
private final ArrayList<Program> mPrograms;
private final ArrayList<Event> mHistory;
private final Handler mHandler;
private int mHistoryMaximumSize;
private final Program mPocketProgram;
private final Program mWave2WakeProgram;
private static class Program {
@NonNull
public final Data[] dataArray;
private static class Data {
public final boolean isNear;
public int timeMin;
public final long timeMax;
public Data(boolean isNear, int timeMin, long timeMax) {
this.isNear = isNear;
this.timeMin = timeMin;
this.timeMax = timeMax;
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
return new HashCodeBuilder(31, 3615)
.append(isNear)
.append(timeMin)
.append(timeMax)
.toHashCode();
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Data))
return false;
Data data = (Data) o;
return new EqualsBuilder()
.append(isNear, data.isNear)
.append(timeMin, data.timeMin)
.append(timeMax, data.timeMax)
.isEquals();
}
}
public Program(@NonNull Data[] dataArray) {
this.dataArray = dataArray;
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
return new HashCodeBuilder(2369, 31)
.append(dataArray)
.toHashCode();
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Program))
return false;
Program program = (Program) o;
return new EqualsBuilder()
.append(dataArray, program.dataArray)
.isEquals();
}
public static int fits(@NonNull Program program, @NonNull ArrayList<Event> history) {
Data[] dataArray = program.dataArray;
int historySize = history.size();
int programSize = dataArray.length;
if (historySize < programSize) {
// Program needs slightly longer history.
return -1;
}
int historyOffset = historySize - programSize;
Event eventPrevious = history.get(historyOffset);
for (int i = 1; i < programSize; i++) {
Data data = dataArray[i - 1];
Event eventFuture = history.get(historyOffset + i);
final long delta = eventFuture.time - eventPrevious.time;
if (eventPrevious.isNear != data.isNear
|| delta <= data.timeMin
|| delta >= data.timeMax) {
return -1;
}
eventPrevious = eventFuture;
}
Data data = dataArray[programSize - 1];
if (eventPrevious.isNear == data.isNear) {
return data.timeMin;
}
return -1;
}
public static class Builder {
private final ArrayList<Data> mProgram = new ArrayList<>(10);
private boolean mLastNear;
@NonNull
public Builder begin(boolean isNear, int timeMin) {
return add(isNear, timeMin, Long.MAX_VALUE);
}
@NonNull
public Builder add(int timeMin, long timeMax) {
return add(!mLastNear, timeMin, timeMax);
}
@NonNull
public Builder end(int timeMin) {
return add(timeMin, 0);
}
@NonNull
private Builder add(boolean isNear, int timeMin, long timeMax) {
Data data = new Data(isNear, timeMin, timeMax);
mProgram.add(data);
mLastNear = isNear;
return this;
}
@NonNull
public Program build() {
return new Program(mProgram.toArray(new Data[mProgram.size()]));
}
}
}
/**
* Proximity event.
*/
private static class Event {
final boolean isNear;
final long time;
public Event(boolean isNear, long time) {
this.isNear = isNear;
this.time = time;
}
}
private ProximitySensor() {
super();
mPocketProgram = new Program.Builder()
.begin(true, POCKET_START_DELAY) /* is near at least for some seconds */
.end(0) /* and after: is far at least for 0 seconds */
.build();
mWave2WakeProgram = new Program.Builder()
.begin(true, 200) /* is near at least for 200 millis */
.add(0, 1500) /* and after: is far not more than 1 second */
.add(0, 1500) /* and after: is near not more than 1 second */
.end(0) /* and after: is far at least for 0 second */
.build();
mPrograms = new ArrayList<>();
mPrograms.add(mPocketProgram);
mPrograms.add(mWave2WakeProgram); // needed to include in history size calculation
for (Program program : mPrograms) {
int size = program.dataArray.length;
if (size > mHistoryMaximumSize) {
mHistoryMaximumSize = size;
}
}
mHistory = new ArrayList<>(mHistoryMaximumSize);
mHandler = new Handler();
}
@NonNull
public static ProximitySensor getInstance() {
ProximitySensor sensor = sProximitySensorWeak != null
? sProximitySensorWeak.get() : null;
if (sensor == null) {
sensor = new ProximitySensor();
sProximitySensorWeak = new WeakReference<>(sensor);
}
return sensor;
}
/**
* @return {@code true} if sensor is currently in "near" state, and {@code false} otherwise.
*/
public static boolean isNear() {
return (getTimeNow() - sLastEventTime < LAST_EVENT_MAX_TIME || sAttached) && sNear;
}
@Override
public int getType() {
return Sensor.TYPE_PROXIMITY;
}
@Override
public void onStart(@NonNull SensorManager sensorManager) {
if (DEBUG) Log.d(TAG, "Starting proximity sensor...");
mHistory.clear();
mHistory.add(new Event(false, getTimeNow()));
Config.getInstance().registerListener(this);
updateWave2WakeProgram();
// Ignore pocket program's start delay,
// so app can act just after it has started.
mFirstChange = true;
mPocketProgram.dataArray[0].timeMin = 0;
Sensor proximitySensor = sensorManager.getDefaultSensor(getType());
sensorManager.registerListener(this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
mMaximumRange = proximitySensor.getMaximumRange();
sAttached = true;
}
@Override
public void onStop() {
if (DEBUG) Log.d(TAG, "Stopping proximity sensor...");
SensorManager sensorManager = getSensorManager();
sensorManager.unregisterListener(this);
mHandler.removeCallbacksAndMessages(null);
mHistory.clear();
Config.getInstance().unregisterListener(this);
}
@Override
public void onSensorChanged(SensorEvent event) {
final float distance = event.values[0];
final boolean isNear = distance < mMaximumRange || distance < 1.0f;
final boolean changed = sNear != (sNear = isNear) || mFirstChange;
synchronized (monitor) {
long now = getTimeNow();
if (DEBUG) {
int historySize = mHistory.size();
String delta = (historySize > 0
? " delta=" + (now - mHistory.get(historySize - 1).time)
: " first_event");
Log.d(TAG + ":Event", "distance=" + distance
+ " is_near=" + isNear
+ " changed=" + changed
+ delta);
}
if (!changed) {
// Well just in cause if proximity sensor is NOT always eventual.
// This should not happen, but who knows... I found maximum
// range buggy enough.
return;
}
while (mHistory.size() >= mHistoryMaximumSize)
mHistory.remove(0);
mHandler.removeCallbacksAndMessages(null);
mHistory.add(new Event(isNear, now));
for (Program program : mPrograms) {
int delay = Program.fits(program, mHistory);
if (delay >= 0) {
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
synchronized (monitor) {
mHandler.removeCallbacksAndMessages(null);
mHistory.clear();
requestWakeUp();
}
}
}, delay);
}
}
if (mFirstChange) {
// Change pocket program back to defaults.
mPocketProgram.dataArray[0].timeMin = POCKET_START_DELAY;
}
sLastEventTime = now;
mFirstChange = false;
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) { /* unused */ }
@Override
public void onConfigChanged(@NonNull ConfigBase config,
@NonNull String key,
@NonNull Object value) {
switch (key) {
case Config.KEY_ACTIVE_MODE_WAVE_TO_WAKE:
updateWave2WakeProgram();
break;
}
}
private void updateWave2WakeProgram() {
synchronized (monitor) {
boolean enabled = Config.getInstance().isActiveModeWaveToWakeEnabled();
if (enabled) {
if (!mPrograms.contains(mWave2WakeProgram)) {
if (DEBUG) Log.d(TAG, "Added the \"Wave to wake\" program");
mPrograms.add(mWave2WakeProgram);
}
} else {
if (DEBUG) Log.d(TAG, "Removed the \"Wave to wake\" program");
mPrograms.remove(mWave2WakeProgram);
}
}
}
}