/* * 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); } } } }