package com.yotadevices.sdk.utils;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.res.Resources;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.AsyncTask;
import android.os.Vibrator;
import android.view.Gravity;
import android.widget.TextView;
import android.widget.Toast;
import com.yotadevices.sdk.R;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Locale;
import java.util.Map;
/**
* RotationAlgorithm - This class is used to catch events when user rotates the phone to switch between front and back screen.
* Refer to <a href="http://developer.yotaphone.com/docs/getting-started/rotation-algorithm/">Rotation Algorithm</a> for more details.
*/
public class RotationAlgorithm implements SensorEventListener {
private static final String TAG = "RotationAlgorithm";
/**
* OnPhoneRotatedListener - This listener can tell when device is rotated on certain side. Can be
* used in {@link RotationAlgorithm#turnScreenOffIfRotated}
*/
public interface OnPhoneRotatedListener {
/**
* onPhoneRotatedToFS - Called when device is rotated on front screen
*/
public void onPhoneRotatedToFS();
/**
* onPhoneRotatedToBS - Called when device is rotated on back screen
*/
public void onPhoneRotatedToBS();
/**
* onRotataionCancelled - Called if user didin't do rotation
*/
public void onRotataionCancelled();
}
/**
* OPTION_START_WITH_BS = 2: Set this flag if rotation algorithm should start with back screen.
*/
public static final int OPTION_START_WITH_BS = 2;
/**
* OPTION_POWER_ON = 4: Set this flag if front screen should be turned on if rotation starts with back screen.
*/
public static final int OPTION_POWER_ON = 4;
/**
* OPTION_NO_UNLOCK = 8: Set this flag if unlock of the back screen is not needed after the rotation.
*/
public static final int OPTION_NO_UNLOCK = 8;
/**
* OPTION_DONT_MONITOR_BACK_ROTATION = 16: Set this flag if monitoring rotation back, after first rotation is happened is not needed.
*/
public static final int OPTION_DONT_MONITOR_BACK_ROTATION = 16;
/**
* OPTION_EXPECT_FIRST_ROTATION_FOR_60SEC = 32: Set this rotation if application needs to wait for 60 seconds for rotation instead of 6.
*/
public static final int OPTION_EXPECT_FIRST_ROTATION_FOR_60SEC = 32;
private static final int TIME_60SEC = 60 * 1000;
private static final int TIME_6SEC = 6 * 1000;
private Context mContext;
private static RotationAlgorithm mInstance;
private OnPhoneRotatedListener mListener = null;
/**
* @param context input context
* @return Instance of RotationAlgorithm
*/
public static RotationAlgorithm getInstance(Context context) {
if (mInstance == null) {
mInstance = new RotationAlgorithm(context);
} else {
mInstance.mStartWithFS = true;
mInstance.mPowerOn = false;
mInstance.mNoUnlock = false;
mInstance.setContext(context);
}
return mInstance;
}
private class SensorAttributes {
public float x;
public float y;
public float z;
}
private class AvgGyroscope {
float xAvg = 0f;
float yAvg = 0f;
float zAvg = 0f;
}
private final static class MyPowerUtils implements IPowerCallback {
private static MyPowerUtils sInstance;
private Context ctx;
public static MyPowerUtils getInstance(Context ctx) {
if (sInstance == null) {
sInstance = new MyPowerUtils(ctx);
}
return sInstance;
}
public void setContext(Context ctx) {
this.ctx = ctx;
}
private MyPowerUtils(Context ctx) {
this.ctx = ctx;
}
@Override
public void goToSleep() {
PowerUtils.goToSleep(ctx);
}
@Override
public void wakeUp() {
PowerUtils.wakeUp(ctx);
}
@Override
public void lockOn() {
PowerUtils.lockOn(ctx);
}
@Override
public void lockOff() {
PowerUtils.lockOff(ctx);
}
@Override
public void lockBackScreen() {
PowerUtils.lockBackScreen(ctx);
}
@Override
public void unlockBackScreen() {
PowerUtils.unlockBackScreen(ctx);
}
}
private boolean mRotationHappened = false;
private boolean mFirstStep = true;
private boolean mUserIsLying = false;
private long p2bClickedTime;
private LinkedList<SensorAttributes> mGyroscopeArray = new LinkedList<SensorAttributes>();
private final static int TIME_DELAY = 50; // time is in milliseconds
private final int MAX_SIZE = 1000 / TIME_DELAY;
private boolean mStartWithFS = true;
private boolean mPowerOn = false;
private boolean mDeviceLockSettingIsNone = false;
private boolean mNoUnlock = false;
private boolean mExpectFirstRotationFor60Sec = false;
private SensorManager mSensorManager;
private KeyguardManager mKeyguardManager;
private IPowerCallback mUtils;
private RotationAlgorithm(Context context) {
mContext = context;
mUtils = MyPowerUtils.getInstance(context);
}
/**
* @hide
*/
public void setPowerCallback(IPowerCallback utils) {
mUtils = utils;
}
/**
* @hide
*/
public void setContext(Context context) {
if (mContext != context) {
mContext = context;
((MyPowerUtils) mUtils).setContext(context);
}
}
/**
* turnScreenOffIfRotated: Main function to call - starts the rotation algorithm
*
* @param options - bitmask of RotationAlgorithm options that can contain
* {@link RotationAlgorithm#OPTION_START_WITH_BS},
* {@link RotationAlgorithm#OPTION_POWER_ON},
* {@link RotationAlgorithm#OPTION_NO_UNLOCK},
* {@link RotationAlgorithm#OPTION_DONT_MONITOR_BACK_ROTATION},
* {@link RotationAlgorithm#OPTION_EXPECT_FIRST_ROTATION_FOR_60SEC}
* @param listener - callback when phone rotation happened
*/
public void turnScreenOffIfRotated(int options, OnPhoneRotatedListener listener) {
mListener = listener;
turnScreenOffIfRotated(options);
}
/**
* turnScreenOffIfRotated - Main function to call: starts the rotation algorithm
*
* @param options - bitmask of RotationAlgorithm options
*/
public void turnScreenOffIfRotated(int options) {
mStartWithFS = !isOptionSet(options, OPTION_START_WITH_BS);
mPowerOn = isOptionSet(options, OPTION_POWER_ON);
mNoUnlock = isOptionSet(options, OPTION_NO_UNLOCK);
mExpectFirstRotationFor60Sec = isOptionSet(options, OPTION_EXPECT_FIRST_ROTATION_FOR_60SEC);
turnScreenOffIfRotated();
}
private boolean isOptionSet(int options, int flag) {
return (options & flag) != 0;
}
/**
* Main function to call - starts the rotation algorithm
*/
public void turnScreenOffIfRotated() {
mRotationHappened = false;
mFirstStep = true;
mUserIsLying = false;
mGyroscopeArray = new LinkedList<SensorAttributes>();
p2bClickedTime = System.currentTimeMillis();
FrameworkUtils.isLockScreenDisabled(mContext, new IPlatinumCallback() {
@Override
public void onLockScreenDisabled(boolean isLockScreenDisabled) {
mDeviceLockSettingIsNone = isLockScreenDisabled;
}
});
mSensorManager = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE);
mSensorManager.registerListener(this, mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), TIME_DELAY * 1000);
mSensorManager.registerListener(this, mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE), TIME_DELAY * 1000);
mKeyguardManager = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
}
/**
* issueStandardToastAndVibration - Use this to issue standard Toast and Vibration that should be used right after user pressed put-to-back button in application. This will prompt the user to rotate the phone.
*/
public void issueStandardToastAndVibration() {
Resources res = mContext.getResources();
// try {
// res = getResourceContext();
// } catch (Exception e) {
// try {
// res = getResourceByPath();
// } catch (Exception e1) {
// }
// }
try {
handleToastAndVibro(res.getString(R.string.application_is_updated_on_bs), res.getInteger(R.integer.vibration_time));
} catch (Exception e) {// null, not found
String locale = Locale.getDefault().getLanguage();
String text = RESOURCE.get(locale);
if (text == null) {
text = RESOURCE.get("en");
}
handleToastAndVibro(text, 18);
}
}
private void handleToastAndVibro(String str, int time) {
Toast toast = Toast.makeText(mContext, str, Toast.LENGTH_SHORT);
((TextView) toast.getView().findViewById(android.R.id.message)).setGravity(Gravity.CENTER);
toast.show();
((Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE)).vibrate(time);
}
// private Resources getResourceContext() throws NameNotFoundException {
// return mContext.getPackageManager().getResourcesForApplication("com.yotadevices.sdk");
// }
//
// private Resources getResourceByPath() throws Exception {
// final WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
// final Display d = wm.getDefaultDisplay();
// Configuration c = new Configuration();
// final DisplayMetrics metrics = new DisplayMetrics();
// d.getMetrics(metrics);
// AssetManager assets = new AssetManager();
// mContext.getResources().getAssets().addAssetPath(mContext.getPackageResourcePath());
// return new Resources(assets, metrics, c);
// }
private final static Map<String, String> RESOURCE = new HashMap<String, String>();
{
RESOURCE.put("en", "Application is updated on Back Screen");
RESOURCE.put("ar", "تم تحديث التطبيق على الشاشة الخلفية");
RESOURCE.put("de", "Die Anwendung wird auf dem rückseitigen Bildschirm aktualisiert");
RESOURCE.put("es", "La aplicación está actualizada en la pantalla trasera");
RESOURCE.put("fr", "L\'application est mise à jour sur l\'écran arrière");
RESOURCE.put("it", "Applicazione aggiornata sullo schermo posteriore");
RESOURCE.put("ru", "Приложение обновлено\nна втором экране");
}
@Override
/**
* @hide
*/
public void onSensorChanged(SensorEvent event) {
SensorAttributes accelerometer = new SensorAttributes();
SensorAttributes gyroscope = new SensorAttributes();
setInitialValue(event, accelerometer, gyroscope);
calculateFlags(accelerometer, getAverageGyroscope(gyroscope));
handleAction();
}
private void setInitialValue(SensorEvent event, SensorAttributes accelerometer, SensorAttributes gyroscope) {
Sensor sensor = event.sensor;
if (sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
accelerometer.x = event.values[0];
accelerometer.y = event.values[1];
accelerometer.z = event.values[2];
} else if (sensor.getType() == Sensor.TYPE_GYROSCOPE) {
gyroscope.x = event.values[0];
gyroscope.y = event.values[1];
gyroscope.z = event.values[2];
}
}
private AvgGyroscope getAverageGyroscope(SensorAttributes gyroscope) {
mGyroscopeArray.add(gyroscope);
if (mGyroscopeArray.size() > MAX_SIZE) {
mGyroscopeArray.poll();
}
AvgGyroscope avgGyroscope = new AvgGyroscope();
for (SensorAttributes atr : mGyroscopeArray) {
avgGyroscope.xAvg += atr.x;
avgGyroscope.yAvg += atr.y;
avgGyroscope.zAvg += atr.z;
}
int size = mGyroscopeArray.size();
avgGyroscope.xAvg = avgGyroscope.xAvg / size;
avgGyroscope.yAvg = avgGyroscope.yAvg / size;
avgGyroscope.zAvg = avgGyroscope.zAvg / size;
return avgGyroscope;
}
private void calculateFlags(SensorAttributes accelerometer, AvgGyroscope avgGyroscope) {
if (mFirstStep) {
if (mStartWithFS && accelerometer.z < -3 || !mStartWithFS && accelerometer.z > 3) {
mUserIsLying = true; //Meaning that user is holding device upside down
}
mFirstStep = false;
}
if (accelerometer.z < -3 && mStartWithFS && !mUserIsLying
|| accelerometer.z > 3 && !mStartWithFS && !mUserIsLying
|| accelerometer.z > 3 && mStartWithFS && mUserIsLying
|| accelerometer.z < -3 && !mStartWithFS && mUserIsLying
|| Math.abs(avgGyroscope.yAvg) > 2
|| Math.abs(avgGyroscope.xAvg) > 2) {
mRotationHappened = true;
}
}
private void handleAction() {
if (System.currentTimeMillis() > (p2bClickedTime + (mExpectFirstRotationFor60Sec ? TIME_60SEC : TIME_6SEC))) {
mSensorManager.unregisterListener(this);
if (mListener != null) {
mListener.onRotataionCancelled();
}
}
if (mRotationHappened) {
mSensorManager.unregisterListener(this);
if (mStartWithFS) {
mUtils.goToSleep();
if (!mNoUnlock) {
mUtils.unlockBackScreen();
}
if (mListener != null) {
mListener.onPhoneRotatedToBS();
}
} else {
if (mPowerOn || mDeviceLockSettingIsNone) {
mUtils.wakeUp();
}
mUtils.lockBackScreen();
new UnlockScreen().execute();
if (mListener != null) {
mListener.onPhoneRotatedToFS();
}
}
}
}
@Override
/**
* @hide
*/
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
// If rotation happens fast then unlock can happen faster than lock is completed.
// This is why we need to perform another unlock operation after some time.
private class UnlockScreen extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
mUtils.wakeUp();
mUtils.lockOff();
int limit = 2000;
while (!mKeyguardManager.inKeyguardRestrictedInputMode() && mPowerOn) {
try {
mUtils.lockOff();
Thread.sleep(50);
limit -= 50;
if (limit < 0)
break;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
mUtils.lockOff();
return null;
}
@Override
protected void onProgressUpdate(Void... progress) {
}
@Override
protected void onPostExecute(Void result) {
}
}
}