public class AdminHandler implements ICommandHandler {
    private final DevicePolicyManager mDPM;

    public AdminHandler(Activity activity) {
        mDPM = (DevicePolicyManager) activity.getSystemService(Context.DEVICE_POLICY_SERVICE);
    }

    @Override
    public boolean handleCommand(Command cmd) {
        final String cmdStr = cmd.getCommand();

        if ("ADMIN_LOCK_SCREEN".equals(cmdStr)) {
            if (mDPM.isAdminActive(AdminReceiver.COMP)) {
                cmd.start();
                mDPM.lockNow();
                cmd.finished();
            } else {
                cmd.failed("device admin privileges missing");
            }
            return true;
        }

        return false;
    }
} public class BluetoothHandler implements ICommandHandler {
    private final Context mContext;
    private final BluetoothManager mManager;

    public BluetoothHandler(Context ctx, BluetoothManager manager) {
        mContext = ctx;
        mManager = manager;
    }

    @Override
    public boolean handleCommand(Command cmd) {
        final String cmdStr = cmd.getCommand();

        if ("BLUETOOTH_ON".equals(cmdStr)) {
            if (ContextCompat.checkSelfPermission(mContext, Manifest.permission.BLUETOOTH_ADMIN) != PackageManager.PERMISSION_GRANTED) {
                cmd.failed("bluetooth permission missing");
            }

            cmd.start();
            mManager.getAdapter().enable();
        } else if ("BLUETOOTH_OFF".equals(cmdStr)) {
            if (ContextCompat.checkSelfPermission(mContext, Manifest.permission.BLUETOOTH_ADMIN) != PackageManager.PERMISSION_GRANTED) {
                cmd.failed("bluetooth permission missing");
            }

            cmd.start();
            mManager.getAdapter().disable();
        } else {
            return false;
        }

        cmd.finished();
        return true;
    }
} public class CommandQueue extends HandlerThread implements IStateUpdateListener {
    private final ServerConnection mServerConnection;

    private final Handler mUiHandler;
    private Handler mWorkerHandler;

    private final ArrayList<ICommandHandler> mHandlers = new ArrayList<>();
    private final CommandLog mCmdLog = new CommandLog();

    public CommandQueue(ServerConnection serverConnection) {
        super("CommandQueue");

        EventBus.getDefault().register(this);

        mServerConnection = serverConnection;
        mUiHandler = new Handler(Looper.getMainLooper());

        start();
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onMessageEvent(CommandLogClient client) {
        client.setCommandLog(mCmdLog);
    }

    public void addHandler(ICommandHandler h) {
        synchronized (mHandlers) {
            if (!mHandlers.contains(h)) {
                mHandlers.add(h);
            }
        }
    }

    @Override
    protected void onLooperPrepared() {
        super.onLooperPrepared();

        mWorkerHandler = new Handler(getLooper(), msg -> {
            if (msg.what == 12) {
                Command cmd = (Command) msg.obj;

                synchronized (mHandlers) {
                    for (ICommandHandler mHandler : mHandlers) {
                        try {
                            if (mHandler.handleCommand(cmd)) {
                                break;
                            }
                        } catch (Throwable t) {
                            cmd.failed(t.getLocalizedMessage());
                            break;
                        }
                    }
                }
            }

            return true;
        });
    }

    @Override
    public void itemUpdated(String name, String value) {
        if (value != null && !value.isEmpty()) {
            Command cmd = new Command(value);

            mUiHandler.post(() -> mCmdLog.add(cmd));
            mWorkerHandler.obtainMessage(12, cmd).sendToTarget();
        }
    }

    public void updateFromPreferences(final SharedPreferences prefs) {
        String mCmdItemName = prefs.getString(Constants.PREF_CMD_ITEM, "");

        mUiHandler.post(() -> mCmdLog.setSize(prefs.getInt(Constants.PREF_CMD_LOG_SIZE, 100)));

        mServerConnection.subscribeCommandItem(this, mCmdItemName);
    }

    public void terminate() {
        EventBus.getDefault().unregister(this);
        quit();
    }
} @TargetApi(Build.VERSION_CODES.M)
public class FlashHandler implements ICommandHandler {
    private static final String TAG = "HPV-FlashHandler";

    private final Pattern BLINK_PATTERN = Pattern.compile("FLASH_BLINK ([0-9]+)");

    private final CameraManager mCameraManager;
    private FlashControlThread controller;
    private String torchId;

    public FlashHandler(CameraManager cameraManager) {
        mCameraManager = cameraManager;

        try {
            for (String camId : mCameraManager.getCameraIdList()) {
                CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(camId);
                Boolean hasFlash = characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
                Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);

                if (facing != null && facing == CameraCharacteristics.LENS_FACING_BACK
                        && Boolean.TRUE.equals(hasFlash)) {
                    torchId = camId;
                    break;
                }
            }

            if (torchId != null) {
                controller = new FlashControlThread();
                controller.start();
            }
        } catch (CameraAccessException e) {
            Log.d(TAG, "Could not create flash controller");
        }
    }

    public boolean isAvailable() {
        return torchId != null;
    }

    @Override
    public boolean handleCommand(Command cmd) {
        final String cmdStr = cmd.getCommand();

        Matcher m = BLINK_PATTERN.matcher(cmdStr);

        if (cmdStr.startsWith("FLASH_") && (controller == null || torchId == null)) {
            cmd.failed("Flash control not available on this device");
        } else if ("FLASH_ON".equals(cmdStr)) {
            cmd.start();
            controller.enableFlash();
        } else if ("FLASH_OFF".equals(cmdStr)) {
            cmd.start();
            controller.disableFlash();
        } else if ("FLASH_BLINK".equals(cmdStr)) {
            cmd.start();
            controller.pulseFlash(1000);
        } else if (m.matches()) {
            String value = m.group(1);

            try {
                int length = Integer.parseInt(value);
                cmd.start();
                controller.pulseFlash(length);
            } catch (NumberFormatException e) {
                cmd.failed("failed to parse length from command");
            }
        } else {
            return false;
        }

        cmd.finished();
        return true;
    }

    public void terminate() {
        if (controller != null) {
            controller.terminate();
            controller = null;
        }
    }

    private class FlashControlThread extends Thread {
        private final AtomicBoolean fRunning = new AtomicBoolean(true);
        private final AtomicInteger fPulseLength = new AtomicInteger(0);
        private final AtomicBoolean fOn = new AtomicBoolean(false);

        private boolean fFlashOn = false;

        private FlashControlThread() {
            super("FlashControlThread");
            setDaemon(true);
        }

        private void terminate() {
            fRunning.set(false);
            try {
                join();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }

        @Override
        public void run() {
            Log.d(TAG, "FlashControlThread started");

            while (fRunning.get()) {
                synchronized (fRunning) {
                    setFlash(fOn.get() || fPulseLength.get() > 0 && !fFlashOn);

                    try {
                        fRunning.wait(fPulseLength.get() == 0 ? 1000 : fPulseLength.get());
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            }

            setFlash(false);
            Log.d(TAG, "FlashControlThread finished");
        }

        private void pulseFlash(int pulseLength) {
            Log.d(TAG, "pulseFlash: pulseLength=" + pulseLength);

            synchronized (fRunning) {
                fPulseLength.set(pulseLength);
                fOn.set(false);

                fRunning.notifyAll();
            }
        }

        private void disableFlash() {
            Log.d(TAG, "disableFlash");

            synchronized (fRunning) {
                fPulseLength.set(0);
                fOn.set(false);

                fRunning.notifyAll();
            }
        }

        private void enableFlash() {
            Log.d(TAG, "disableFlash");

            synchronized (fRunning) {
                fPulseLength.set(0);
                fOn.set(true);

                fRunning.notifyAll();
            }
        }

        private void setFlash(boolean flashing) {
            if (flashing != fFlashOn) {
                fFlashOn = flashing;

                try {
                    if (torchId != null) {
                        mCameraManager.setTorchMode(torchId, flashing);
                        Log.d(TAG, "Set torchmode " + flashing);
                    }
                } catch (CameraAccessException e) {
                    if (e.getReason() != CameraAccessException.MAX_CAMERAS_IN_USE) {
                        Log.e(TAG, "Failed to toggle flash!", e);
                    }
                }
            }
        }
    }
} public class InternalCommandHandler implements ICommandHandler {
    private static final String TAG = "HPV-InternalCommandHa";

    private final Pattern START_PATTERN = Pattern.compile("START_APP (.+)");
    private final Pattern CAPTURE_SCREEN_PATTERN = Pattern.compile("CAPTURE_SCREEN (\\S+)( [0-9]+)?");
    private final Pattern CAPTURE_CAMERA_PATTERN = Pattern.compile("CAPTURE_CAMERA (\\S+)( [0-9]+)?");

    private final MainActivity mActivity;
    private final ServerConnection mConnection;
    private final IMotionDetector mMotionDetector;
    private int takePictureDelay;

    public InternalCommandHandler(MainActivity mainActivity,
                                  IMotionDetector motionDetector,
                                  ServerConnection connection) {
        mActivity = mainActivity;
        mMotionDetector = motionDetector;
        mConnection = connection;

        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mActivity);
        try {
            takePictureDelay = Integer.parseInt(prefs.getString(Constants.PREF_TAKE_PIC_DELAY, "100"));
        } catch (NumberFormatException e) {
            takePictureDelay = 100;
            Log.e(TAG, "could not parse pref_take_pic_delay value " + prefs.getString(Constants.PREF_TAKE_PIC_DELAY, "100") + ". using default 100");
        }
    }

    @Override
    public boolean handleCommand(Command cmd) {
        final String cmdStr = cmd.getCommand();

        String[] paras;

        if ("RESTART".equals(cmdStr)) {
            cmd.start();
            EventBus.getDefault().post(new Constants.Restart());
        } else if ("TO_FRONT".equals(cmdStr)) {
            Intent intent = new Intent(mActivity, MainActivity.class);
            intent.setAction(Intent.ACTION_MAIN);
            intent.addCategory(Intent.CATEGORY_LAUNCHER);
            mActivity.startActivity(intent);
        } else if ("UPDATE_ITEMS".equals(cmdStr)) {
            cmd.start();
            mConnection.sendCurrentValues();
        } else if ("ENABLE_MOTION_DETECTION".equals(cmdStr)) {
            cmd.start();
            setMotionDetectionEnabled(true);
        } else if ("DISABLE_MOTION_DETECTION".equals(cmdStr)) {
            cmd.start();
            setMotionDetectionEnabled(false);
        } else if ((paras = matchesRegexp(CAPTURE_SCREEN_PATTERN, cmdStr)) != null) {
            ScreenCapturer c = mActivity.getCapturer();
            if (c == null) {
                cmd.failed("could not create capture class. Has the permission been granted?");
            } else {
                cmd.start();

                int compQuality = getQuality(paras);

                Bitmap bmp = c.captureScreen();
                ByteArrayOutputStream os = new ByteArrayOutputStream();
                bmp.compress(Bitmap.CompressFormat.JPEG, compQuality, os);

                byte[] bytes = os.toByteArray();

                mConnection.updateJpeg(paras[0], bytes);
            }
        } else if ((paras = matchesRegexp(CAPTURE_CAMERA_PATTERN, cmdStr)) != null) {
            cmd.start();

            final String p = paras[0];
            final int compQuality = getQuality(paras);

            mMotionDetector.getCamera().takePicture(new ICamera.IPictureListener() {
                @Override
                public void picture(byte[] data) {
                    mConnection.updateJpeg(p, data);
                    cmd.finished();
                }

                @Override
                public void error(String message) {
                    cmd.failed(message);
                }

                @Override
                public void progress(String message) {
                    cmd.progress(message);
                }
            }, takePictureDelay, compQuality);

            return true;
        } else if ((paras = matchesRegexp(START_PATTERN, cmdStr)) != null) {
            Intent launchIntent = mActivity.getPackageManager().getLaunchIntentForPackage(paras[0]);

            if (launchIntent != null) {
                cmd.start();
                mActivity.startActivity(launchIntent);
            } else {
                cmd.failed("could not find app for package " + paras[0]);
            }
        } else {
            return false;
        }

        cmd.finished();
        return true;
    }

    private int getQuality(String[] paras) {
        if (paras.length > 1 && paras[1] != null) {
            return Integer.parseInt(paras[1]);
        }

        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mActivity);
        return Integer.parseInt(prefs.getString(Constants.PREF_JPEG_QUALITY, "70"));
    }

    private void setMotionDetectionEnabled(boolean enabled) {
        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mActivity);

        SharedPreferences.Editor editor1 = prefs.edit();
        editor1.putBoolean(Constants.PREF_MOTION_DETECTION_ENABLED, enabled);
        editor1.apply();

        mActivity.runOnUiThread(mActivity::updateMotionPreferences);
    }

    private String[] matchesRegexp(Pattern pattern, String text) {
        Matcher m = pattern.matcher(text);

        if (m.matches()) {
            String name = m.group(1);
            String num = null;

            if (m.groupCount() > 1) {
                num = m.group(2);
            }

            if (num != null) {
                return new String[]{name, num.substring(1)};
            } else {
                return new String[]{name};
            }
        }

        return null;
    }
} public class NotificationHandler implements ICommandHandler {
    private static final String CHANNEL_ID = "de.vier_bier.habpanelviewer";

    private static final Pattern SHOW_PATTERN = Pattern.compile("NOTIFICATION_SHOW ([a-zA-Z]+)(.*)");
    private static final Pattern HIDE_PATTERN = Pattern.compile("NOTIFICATION_HIDE ([a-zA-Z]+)?");

    private final Context mCtx;
    private final NotificationManager mNotificationManager;

    public NotificationHandler(Context ctx) {
        mCtx = ctx.getApplicationContext();

        mNotificationManager = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE);
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
            for (NotificationChannel c : mNotificationManager.getNotificationChannels()) {
                String id = c.getId();

                // getNotificationChannels also returns the notification of the foreground service.
                // so before deleting make sure it is one of the notification channels.
                for (NotificationColor col : NotificationColor.values()) {
                    if (id.equals(CHANNEL_ID + "." + col.name())) {
                        mNotificationManager.deleteNotificationChannel(c.getId());
                    }
                }
            }

            for (NotificationColor c : NotificationColor.values()) {
                createChannel(c.name(), c.color());
            }
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.O)
    private void createChannel(String channelName, int color) {
        NotificationChannel channel = new NotificationChannel(CHANNEL_ID + "." + channelName, channelName, NotificationManager.IMPORTANCE_DEFAULT);
        channel.enableLights(true);
        channel.setSound(null, null);
        channel.setLightColor(color);
        channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
        mNotificationManager.createNotificationChannel(channel);
    }

    @Override
    public boolean handleCommand(Command cmd) {
        final String cmdStr = cmd.getCommand();

        Matcher m = SHOW_PATTERN.matcher(cmdStr);
        Matcher m2 = HIDE_PATTERN.matcher(cmdStr);
        if (cmdStr.startsWith("NOTIFICATION_") && mNotificationManager == null) {
            cmd.failed("Android 8 or newer needed");
        } else if (m.matches()) {
            cmd.start();

            final String colorStr = m.group(1).toLowerCase();
            try {
                final String message = m.group(2);

                final NotificationColor color = NotificationColor.valueOf(colorStr);
                NotificationCompat.Builder builder = new NotificationCompat.Builder(mCtx, CHANNEL_ID + "." + color.name());
                builder.setSmallIcon(R.drawable.logo);
                if (message != null) {
                    builder.setContentTitle(message.trim());
                }
                builder.setSound(null);
                builder.setLights(color.color(), 1000, 1000);

                mNotificationManager.notify(color.ordinal(), builder.build());
            } catch (IllegalArgumentException e) {
                cmd.failed("invalid color: " + colorStr);
            }
        } else if (m2.matches()) {
            cmd.start();
            final String colorStr = m2.group(1).toLowerCase();
            try {
                final NotificationColor color = NotificationColor.valueOf(colorStr);

                mNotificationManager.cancel(color.ordinal());
            } catch (IllegalArgumentException e) {
                cmd.failed("invalid color: " + colorStr);
            }
        } else if ("NOTIFICATION_HIDE".equals(cmdStr)) {
            cmd.start();
            for (NotificationColor c : NotificationColor.values()) {
                mNotificationManager.cancel(c.ordinal());
            }
        } else {
            return false;
        }

        cmd.finished();
        return true;
    }

    enum NotificationColor {
        white {
            @Override
            public int color() {
                return 0xffffffff;
            }
        }, red {
            @Override
            public int color() {
                return 0xffff0000;
            }
        }, green {
            @Override
            public int color() {
                return 0xff00ff00;
            }
        }, blue {
            @Override
            public int color() {
                return 0xff0000ff;
            }
        };

        public abstract int color();
    }
} public class ScreenHandler implements ICommandHandler {
    private final Pattern SET_PATTERN = Pattern.compile("SET_BRIGHTNESS (AUTO|[01]?[0-9]?[0-9])");
    private final Pattern SCREEN_ON_PATTERN = Pattern.compile("SCREEN_ON ([0-9]+)");

    private final PowerManager.WakeLock screenOnLock;
    private final Activity mActivity;
    private final DimListener mDimListener;
    private boolean mKeepScreenOn;

    public ScreenHandler(PowerManager pwrManager, Activity activity, DimListener l) {
        mActivity = activity;
        mDimListener = l;
        screenOnLock = pwrManager.newWakeLock(
                PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP
                        | PowerManager.ON_AFTER_RELEASE, "HPV:ScreenOnWakeLock");

        setKeepScreenOn(false);
    }

    @Override
    public boolean handleCommand(Command cmd) {
        final String cmdStr = cmd.getCommand();
        Matcher m1 = SET_PATTERN.matcher(cmdStr);
        Matcher m2 = SCREEN_ON_PATTERN.matcher(cmdStr);

        if ("SCREEN_ON".equals(cmdStr)) {
            cmd.start();
            screenOn(0);
            screenDim(false);
        } else if ("ALLOW_SCREEN_OFF".equals(cmdStr)) {
            cmd.start();
            setKeepScreenOn(false);
        } else if ("KEEP_SCREEN_ON".equals(cmdStr)) {
            cmd.start();
            screenOn(0);
            setKeepScreenOn(true);
        } else if ("SCREEN_DIM".equals(cmdStr)) {
            cmd.start();
            screenDim(true);
        } else if (m1.matches()) {
            String value = m1.group(1);

            try {
                int brightness = -100;
                if (!"AUTO".equals(value)) {
                    brightness = Integer.parseInt(value);
                }

                cmd.start();
                ScreenControllingActivity.setBrightness(mActivity, brightness / 100f);
            } catch (NumberFormatException e) {
                cmd.failed("failed to parse brightness from command");
            }
        } else if (m2.matches()) {
            String value = m2.group(1);

            try {
                int seconds = Integer.parseInt(value);
                cmd.start();
                screenOn(seconds);
            } catch (NumberFormatException e) {
                cmd.failed("failed to parse duration from command. Using 0 seconds.");
                screenOn(0);
            }

            screenDim(false);
        } else {
            return false;
        }

        cmd.finished();
        return true;
    }

    private void screenDim(final boolean dim) {
        Intent intent = new Intent();
        intent.setClass(mActivity, EmptyActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
        intent.putExtra(Constants.INTENT_FLAG_DIM, dim);
        intent.putExtra(Constants.INTENT_FLAG_KEEP_SCREEN_ON, mKeepScreenOn);
        if (dim) {
            mDimListener.deviceDimming();
        }
        mActivity.startActivityForResult(intent, 0);
    }

    private void setKeepScreenOn(final boolean on) {
        mKeepScreenOn = on;
        ScreenControllingActivity.setKeepScreenOn(mActivity, on);
    }

    private synchronized void screenOn(int durationSeconds) {
        screenOnLock.acquire(durationSeconds == 0 ? 500 : durationSeconds * 1000);
    }

    public interface DimListener {
        void deviceDimming();
    }
} public class TtsHandler implements ICommandHandler {
    private final Pattern TTS_PATTERN = Pattern.compile("TTS_(SPEAK|SET_LANG) (.*)");

    private final Context mContext;

    private TextToSpeech mTTS;
    private int mStatus;

    public TtsHandler(Context ctx) {
        mContext = ctx;
    }

    @Override
    public boolean handleCommand(Command cmd) {
        final String cmdStr = cmd.getCommand();

        Matcher m = TTS_PATTERN.matcher(cmdStr);
        if (m.matches()) {
            cmd.start();

            final String ttsCmd = m.group(1);
            final String arg = m.group(2);

            if (mTTS == null) {
                mTTS = new TextToSpeech(mContext, i -> {
                    mStatus = i;

                    doHandleCommand(ttsCmd, arg, cmd);
                });
            } else {
                doHandleCommand(ttsCmd, arg, cmd);
            }
        }

        return m.matches();
    }

    private void doHandleCommand(String ttsCmd, String arg, Command cmd) {
        if (mStatus == TextToSpeech.SUCCESS) {
            if ("SPEAK".equalsIgnoreCase(ttsCmd)) {
                mTTS.speak(arg, TextToSpeech.QUEUE_ADD, null);
            } else {
                Locale l = new Locale(arg);

                if (mTTS.isLanguageAvailable(l) >= TextToSpeech.LANG_AVAILABLE) {
                    mTTS.setLanguage(l);
                } else {
                    cmd.failed("Given locale invalid: " + arg);
                }
            }
            cmd.finished();
        } else {
            cmd.failed("Could not initialize TTS engine!");
        }
    }
} public class VolumeHandler implements ICommandHandler {
    private final Pattern SET_PATTERN = Pattern.compile("SET_VOLUME ([0-9]+)");

    private final Context mCtx;
    private final AudioManager mAudioManager;

    private final int mMaxVolume;
    private int mVolume = -1;

    public VolumeHandler(Context ctx, AudioManager audioManager) {
        mCtx = ctx;
        mAudioManager = audioManager;
        mMaxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
    }

    @Override
    public boolean handleCommand(Command cmd) {
        final String cmdStr = cmd.getCommand();

        Matcher m = SET_PATTERN.matcher(cmdStr);

        if ("MUTE".equals(cmdStr)) {
            cmd.start();
            mVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
            mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 0, AudioManager.FLAG_SHOW_UI);
        } else if ("UNMUTE".equals(cmdStr)) {
            if (mVolume == -1) {
                cmd.failed(mCtx.getString(R.string.device_not_muted));
            } else {
                cmd.start();
                mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, mVolume, AudioManager.FLAG_SHOW_UI);
                mVolume = -1;
            }
        } else if (m.matches()) {
            mVolume = -1;
            String value = m.group(1);

            try {
                int volume = Integer.parseInt(value);
                if (volume < 0) {
                    volume = 0;
                } else if (volume > mMaxVolume) {
                    volume = mMaxVolume;
                }

                cmd.start();
                mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, AudioManager.FLAG_SHOW_UI);
            } catch (NumberFormatException e) {
                cmd.failed("failed to parse volume from command");
            }
        } else {
            return false;
        }

        cmd.finished();
        return true;
    }
} public class WebViewHandler implements ICommandHandler {
    private final Pattern SHOW_PATTERN = Pattern.compile("SHOW_(URL|DASHBOARD) (.+)");

    private final ClientWebView mWebView;

    public WebViewHandler(ClientWebView webView) {
        mWebView = webView;
    }

    @Override
    public boolean handleCommand(Command cmd) {
        final String cmdStr = cmd.getCommand();

        final Matcher m = SHOW_PATTERN.matcher(cmdStr);
        if (m.matches()) {
            cmd.start();
            final String type = m.group(1);

            mWebView.post(() -> {
                if ("URL".matches(type)) {
                    mWebView.loadUrl(m.group(2));
                } else {
                    mWebView.loadDashboard(m.group(2));
                }
                cmd.finished();
            });
        } else if ("SHOW_START_URL".equals(cmdStr)) {
            cmd.start();
            mWebView.post(() -> {
                mWebView.loadStartUrl();
                cmd.finished();
            });
        } else if ("RELOAD".equals(cmdStr)) {
            cmd.start();
            mWebView.post(() -> {
                mWebView.reload();
                cmd.finished();
            });
        } else {
            return false;
        }

        return true;
    }
} public class CommandLogActivity extends ScreenControllingActivity {
    private ScheduledExecutorService executor;
    private CommandInfoAdapter adapter;

    private final CommandLogClient logClient = this::installAdapter;
    private MenuItem mClearItem;

    private void installAdapter(CommandLog cmdLog) {
        final ListView listView = findViewById(R.id.command_log_listview);

        adapter = new CommandInfoAdapter(this, cmdLog.getCommands());
        cmdLog.addListener(adapter);
        listView.setAdapter(adapter);

        listView.setOnItemClickListener((arg0, arg1, position, arg3) -> {
            cmdLog.getCommands().get(position).toggleShowDetails();
            adapter.notifyDataSetChanged();
        });
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        executor = Executors.newSingleThreadScheduledExecutor();
        executor.scheduleWithFixedDelay(() -> runOnUiThread(() -> {
            if (adapter != null) {
                adapter.notifyDataSetChanged();
            }
            if (mClearItem != null && mClearItem.isEnabled() != adapter.getCount() > 0) {
                mClearItem.setEnabled(adapter.getCount() > 0);
            }
        }), 0, 1, TimeUnit.SECONDS);

        setContentView(R.layout.activity_commandlog);

        Toolbar myToolbar = findViewById(R.id.toolbar);
        setSupportActionBar(myToolbar);
        ActionBar ab = getSupportActionBar();
        if (ab != null) {
            ab.setDisplayHomeAsUpEnabled(true);
        }

        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext());
        String theme = prefs.getString(Constants.PREF_THEME, "dark");

        if ("dark".equals(theme)) {
            myToolbar.setPopupTheme(R.style.ThemeOverlay_AppCompat_Dark);
        } else {
            myToolbar.setPopupTheme(R.style.ThemeOverlay_AppCompat_Light);
        }

        EventBus.getDefault().post(logClient);
    }

    @Override
    protected void onDestroy() {
        if (executor != null) {
            executor.shutdown();
            executor = null;
        }

        super.onDestroy();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.commandlog_toolbar_menu, menu);
        mClearItem = menu.findItem(R.id.action_clear_log);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();

        if (id == R.id.action_clear_log) {
            adapter.clear();

            if (mClearItem != null && mClearItem.isEnabled()) {
                mClearItem.setEnabled(false);
            }

            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    @Override
    public View getScreenOnView() {
        return findViewById(R.id.command_log_listview);
    }

    private class CommandInfoAdapter extends BaseAdapter implements CommandLog.CommandLogListener {
        private final Activity mContext;
        private final ArrayList<Command> mCommands;
        private final DateFormat mFormat = SimpleDateFormat.getDateTimeInstance(SimpleDateFormat.SHORT, SimpleDateFormat.MEDIUM);

        CommandInfoAdapter(Activity context, ArrayList<Command> commands) {
            mContext = context;
            mCommands = commands;
        }

        @Override
        public int getCount() {
            return mCommands.size();
        }

        @Override
        public Command getItem(int i) {
            return mCommands.get(i);
        }

        @Override
        public long getItemId(int i) {
            return mCommands.get(i).getTime();
        }

        @NonNull
        @Override
        public View getView(int position, View view, @NonNull ViewGroup parent) {
            LayoutInflater inflater = mContext.getLayoutInflater();
            View rowView = inflater.inflate(R.layout.row_commandlog, parent, false);

            TextView txtTitle = rowView.findViewById(R.id.name);
            TextView txtValue = rowView.findViewById(R.id.value);

            Command cmd = getItem(position);
            txtTitle.setText(cmd.getCommand());

            String txt = mFormat.format(new Date(cmd.getTime()));
            txtValue.setTextColor(cmd.getStatus().getColor());
            if (cmd.hasVisibleDetails()) {
                txt += "\n" + cmd.getDetails();
            }
            txtValue.setText(txt);

            return rowView;
        }

        @Override
        public CharSequence[] getAutofillOptions() {
            return new CharSequence[0];
        }

        @Override
        public void logChanged() {
            runOnUiThread(this::notifyDataSetChanged);
        }

        void clear() {
            mCommands.clear();
            logChanged();
        }
    }
} public class ConnectionStatistics {
    private final Context mCtx;
    private final long mStartTime = System.currentTimeMillis();
    private State mState = State.DISCONNECTED;

    private long mLastOnlineTime = -1;
    private long mLastOfflineTime = mStartTime;
    private long mOfflinePeriods = 0;
    private long mOfflineMillis = 0;
    private long mOfflineMaxMillis = 0;
    private long mOfflineAverage = 0;
    private long mOnlinePeriods = 0;
    private long mOnlineMillis = 0;
    private long mOnlineMaxMillis = 0;
    private long mOnlineAverage = 0;

    public ConnectionStatistics(Context context) {
        mCtx = context;
        EventBus.getDefault().register(this);
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onMessageEvent(ApplicationStatus status) {
        long now = System.currentTimeMillis();

        long currentOnlineTime = mState == State.CONNECTED ? now - mLastOnlineTime : 0;
        long currentOfflineTime = mState == State.DISCONNECTED ? now - mLastOfflineTime : 0;
        long averageOnlineTime = mState == State.CONNECTED ? (mOnlineAverage * mOnlinePeriods + currentOnlineTime) / (mOnlinePeriods + 1) : mOnlineAverage;
        long averageOfflineTime = mState == State.DISCONNECTED ? (mOfflineAverage * mOfflinePeriods + currentOfflineTime) / (mOfflinePeriods + 1) : mOfflineAverage;

        status.set(mCtx.getString(R.string.connectionStatistics),
                mCtx.getString(R.string.connectionDetails,
                        toDuration(now - mStartTime),
                        toDuration(mOnlineMillis + currentOnlineTime),
                        (mOnlinePeriods + (mState == State.CONNECTED ? 1 : 0)),
                        toDuration(Math.max(currentOnlineTime, mOnlineMaxMillis)),
                        toDuration(averageOnlineTime),
                        toDuration(mOfflineMillis + currentOfflineTime),
                        (mOfflinePeriods + (mState == State.DISCONNECTED ? 1 : 0)),
                        toDuration(Math.max(currentOfflineTime, mOfflineMaxMillis)),
                        toDuration(averageOfflineTime)));
    }

    private String toDuration(long durationMillis) {
        if (durationMillis < 1000) {
            return "-";
        }

        Resources res = mCtx.getResources();

        String retVal = "";

        // days
        if (durationMillis > 86400000) {
            long days = durationMillis / 86400000;

            retVal += res.getQuantityString(R.plurals.days, (int) days, days);
            durationMillis %= 86400000;
        }

        // hours
        if (durationMillis > 3600000) {
            long hours = durationMillis / 3600000;

            retVal += res.getQuantityString(R.plurals.hours, (int) hours, hours);
            durationMillis %= 3600000;
        }

        // minutes
        if (durationMillis > 60000) {
            long minutes = durationMillis / 60000;

            retVal += res.getQuantityString(R.plurals.minutes, (int) minutes, minutes);
            durationMillis %= 60000;
        }

        // seconds
        if (durationMillis > 1000) {
            long seconds = durationMillis / 1000;

            retVal += res.getQuantityString(R.plurals.seconds, (int) seconds, seconds);
        }

        return retVal.substring(0, retVal.length() - 1);
    }

    public synchronized void disconnected() {
        if (mState == State.CONNECTED) {
            mLastOfflineTime = System.currentTimeMillis();

            long duration = mLastOfflineTime - mLastOnlineTime;
            mOnlineMillis += duration;

            if (duration > mOnlineMaxMillis) {
                mOnlineMaxMillis = duration;
            }

            mOnlineAverage = (mOnlineAverage * mOnlinePeriods + duration) / ++mOnlinePeriods;
            mState = State.DISCONNECTED;
        }
    }

    public synchronized void connected() {
        if (mState == State.DISCONNECTED) {
            mLastOnlineTime = System.currentTimeMillis();

            if (mLastOnlineTime > -1) {
                long duration = mLastOnlineTime - mLastOfflineTime;
                mOfflineMillis += duration;

                if (duration > mOfflineMaxMillis) {
                    mOfflineMaxMillis = duration;
                }

                mOfflineAverage = (mOfflineAverage * mOfflinePeriods + duration) / ++mOfflinePeriods;
                mState = State.CONNECTED;
            }
        }
    }

    public void terminate() {
        EventBus.getDefault().unregister(this);
    }

    private enum State {
        CONNECTED, DISCONNECTED
    }

} public class OkHttpClientFactory {
    private static OkHttpClientFactory ourInstance;

    public Credentials mCred = null;
    private String mHost;
    private String mRealm;

    public static synchronized OkHttpClientFactory getInstance() {
        if (ourInstance == null) {
            ourInstance = new OkHttpClientFactory();
        }
        return ourInstance;
    }

    public OkHttpClient create() {
        OkHttpClient.Builder builder = new OkHttpClient.Builder().readTimeout(0, TimeUnit.SECONDS);

        builder.sslSocketFactory(CertificateManager.getInstance().getSocketFactory(),
                CertificateManager.getInstance().getTrustManager())
                .hostnameVerifier((s, session) -> {
                    try {
                        Certificate[] certificates = session.getPeerCertificates();
                        for (Certificate certificate : certificates) {
                            if (!(certificate instanceof X509Certificate)) {
                                return false;
                            }
                            if (CertificateManager.getInstance().isTrusted((X509Certificate) certificate)) {
                                return true;
                            }
                        }
                    } catch (SSLException e) {
                        // return false;
                    }
                    return false;
                });

        if (mCred != null) {
            // create a copy so it can not be nulled again
            Credentials copy = new Credentials(mCred.getUserName(), mCred.getPassword());

            final Map<String, CachingAuthenticator> authCache = new ConcurrentHashMap<>();
            final BasicAuthenticator basicAuthenticator = new BasicAuthenticator(copy);
            final DigestAuthenticator digestAuthenticator = new DigestAuthenticator(copy);

            // note that all auth schemes should be registered as lowercase!
            DispatchingAuthenticator authenticator = new DispatchingAuthenticator.Builder()
                    .with("digest", digestAuthenticator)
                    .with("basic", basicAuthenticator)
                    .build();

            builder.authenticator(new CachingAuthenticatorDecorator(authenticator, authCache))
                    .addInterceptor(new AuthenticationCacheInterceptor(authCache));
        }

        return builder.build();
    }

    public void setAuth(String user, String pass) {
        mCred = new Credentials(user, pass);
    }

    public void removeAuth() {
        mCred = null;
    }

    public void setHost(String host) {
        mHost = host;
    }

    public void setRealm(String realm) {
        mRealm = realm;
    }

    public String getHost() {
        return mHost;
    }

    public String getRealm() {
        return mRealm;
    }
} public class CertificateManager {
    private static final String TAG = "HPV-CertificateManager";
    private static CertificateManager mInstance;

    private String mTrustStorePasswd = "secret";
    private final ArrayList<ICertChangedListener> mListeners = new ArrayList<>();

    private File mLocalTrustStoreFile;
    private LocalTrustManager mTrustManager;
    private SSLContext mSslContext;

    private boolean mInitSuccess;

    @TestOnly
    public static synchronized CertificateManager get(File certFile, String passwd) throws GeneralSecurityException {
        CertificateManager cm = new CertificateManager();
        cm.setTrustStore(certFile, passwd);
        return cm;
    }

    public static synchronized CertificateManager getInstance() {
        if (mInstance == null) {
            mInstance = new CertificateManager();

            File certFile = new File(Environment.getDataDirectory()
                    + "/data/de.vier_bier.habpanelviewer/files/localTrustStore.bks");

            try {
                mInstance.setTrustStore(certFile);
            } catch (GeneralSecurityException e) {
                Log.e(TAG, "Certificate store initialization failed", e);
            }
        }

        return mInstance;
    }

    @TestOnly
    private synchronized void setTrustStore(File storeFile, String passwd) throws GeneralSecurityException {
        mTrustStorePasswd = passwd;
        setTrustStore(storeFile);
    }

    public boolean isInitialized() {
        return mInitSuccess;
    }

    private synchronized void setTrustStore(File storeFile) throws GeneralSecurityException {
        mLocalTrustStoreFile = storeFile;
        if (!mLocalTrustStoreFile.exists()) {
            throw new IllegalArgumentException("Given file does not exist: " + storeFile.getAbsolutePath());
        }

        System.setProperty("javax.net.ssl.trustStore", mLocalTrustStoreFile.getAbsolutePath());

        createSslContext();
        HttpsURLConnection.setDefaultSSLSocketFactory(mSslContext.getSocketFactory());
        mInitSuccess = true;
    }

    public synchronized X509TrustManager getTrustManager() {
        return mTrustManager;
    }


    public synchronized SSLSocketFactory getSocketFactory() {
        return mSslContext.getSocketFactory();
    }

    public synchronized void addCertificate(SslCertificate certificate) throws GeneralSecurityException, IOException {
        KeyStore localTrustStore = loadTrustStore();
        X509Certificate x509Certificate = getX509CertFromSslCertHack(certificate);

        String alias = hashName(x509Certificate.getSubjectX500Principal());
        localTrustStore.setCertificateEntry(alias, x509Certificate);

        saveTrustStore(localTrustStore);

        // notify listeners
        synchronized (mListeners) {
            for (ICertChangedListener l : mListeners) {
                l.certAdded();
            }
        }
    }

    public void addCertListener(ICertChangedListener l) {
        synchronized (mListeners) {
            if (!mListeners.contains(l)) {
                mListeners.add(l);
            }
        }
    }

    public void removeCertListener(ICertChangedListener l) {
        synchronized (mListeners) {
            mListeners.remove(l);
        }
    }

    public synchronized boolean isTrusted(X509Certificate cert) {
        if (mTrustManager == null) {
            KeyStore trustStore = loadTrustStore();
            mTrustManager = new LocalTrustManager(trustStore);
        }

        try {
            mTrustManager.checkClientTrusted(new X509Certificate[]{cert}, "generic");
        } catch (CertificateException e) {
            return false;
        }

        return true;
    }

    public synchronized boolean isTrusted(SslCertificate cert) {
        if (mTrustManager == null) {
            KeyStore trustStore = loadTrustStore();
            mTrustManager = new LocalTrustManager(trustStore);
        }

        try {
            mTrustManager.checkClientTrusted(new X509Certificate[]{getX509CertFromSslCertHack(cert)}, "generic");
        } catch (CertificateException e) {
            return false;
        }

        return true;
    }

    private synchronized void createSslContext() throws GeneralSecurityException {
        if (mSslContext == null) {
            if (mTrustManager == null) {
                KeyStore trustStore = loadTrustStore();
                mTrustManager = new LocalTrustManager(trustStore);
            }

            TrustManager[] tms = new TrustManager[]{mTrustManager};

            mSslContext = SSLContext.getInstance("TLS");
            mSslContext.init(null, tms, null);
        }
    }

    private KeyStore loadTrustStore() {
        try {
            KeyStore localTrustStore = KeyStore.getInstance(KeyStore.getDefaultType());
            try (InputStream in = new FileInputStream(mLocalTrustStoreFile)) {
                localTrustStore.load(in, mTrustStorePasswd.toCharArray());
            }

            return localTrustStore;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private X509Certificate getX509CertFromSslCertHack(SslCertificate sslCert) {
        X509Certificate x509Certificate;

        Bundle bundle = SslCertificate.saveState(sslCert);
        byte[] bytes = bundle.getByteArray("x509-certificate");

        if (bytes == null) {
            x509Certificate = null;
        } else {
            try {
                CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
                Certificate cert = certFactory.generateCertificate(new ByteArrayInputStream(bytes));
                x509Certificate = (X509Certificate) cert;
            } catch (CertificateException e) {
                x509Certificate = null;
            }
        }

        return x509Certificate;
    }

    private void saveTrustStore(KeyStore localTrustStore)
            throws IOException, GeneralSecurityException {
        FileOutputStream out = new FileOutputStream(mLocalTrustStoreFile);
        localTrustStore.store(out, mTrustStorePasswd.toCharArray());

        // reset fields so the keystore gets read again
        mTrustManager = null;
        mSslContext = null;

        createSslContext();
    }

    private String hashName(X500Principal principal) {
        try {
            byte[] digest = MessageDigest.getInstance("MD5").digest(
                    principal.getEncoded());

            return Integer.toString(leInt(digest), 16);
        } catch (NoSuchAlgorithmException e) {
            throw new AssertionError(e);
        }
    }

    private int leInt(byte[] bytes) {
        int offset = 0;
        return ((bytes[offset++] & 0xff))
                | ((bytes[offset++] & 0xff) << 8)
                | ((bytes[offset++] & 0xff) << 16)
                | ((bytes[offset] & 0xff) << 24);
    }

    public interface ICertChangedListener {
        void certAdded();
    }
} public class CredentialManager extends Thread {
    private static CredentialManager ourInstance;

    private Handler mHandler;
    private AppDatabase mDb;
    private final HashSet<Credential> mSendCreds = new HashSet<>();
    private boolean mDatabaseUsed = true;

    public static synchronized CredentialManager getInstance() {
        if (ourInstance == null) {
            ourInstance = new CredentialManager();
            ourInstance.start();
        }
        return ourInstance;
    }

    @Override
    public void run() {
        Looper.prepare();

        mHandler = new Handler();

        Looper.loop();
    }

    public void clearCredentials() {
        mHandler.post(() -> {
            if (mDb != null) {
                mDb.clearAllTables();
            }
        });
    }

    public void removeCredentials(String host, String realm) {
        mHandler.post(() -> {
            if (mDb != null) {
                mDb.credentialDao().remove(host, realm);
            }
        });
    }

    public void handleAuthRequest(Context ctx, String host, String realm, final CredentialsListener l) {
        mHandler.post(() -> {
            CountDownLatch latch = new CountDownLatch(1);
            handleAuthRequest(ctx, host, realm, l, latch);
            try {
                latch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }

    private void handleAuthRequest(Context ctx, String host, String realm, final CredentialsListener l, final CountDownLatch latch) {
        if (!hasDatabase() && isDatabaseUsed()) {
            // we have an encrypted, not yet opened database
            UiUtil.showMasterPasswordDialog(ctx, new UiUtil.CredentialsListener() {
                @Override
                public void credentialsEntered(String host2, String realm2, String user, String password, boolean store) {
                    new AsyncTask<Void, Void, Void>() {
                        @Override
                        protected Void doInBackground(Void... voids) {
                            openDb(ctx, password);
                            handleAuthRequest(ctx, host, realm, l, latch);
                            return null;
                        }
                    }.execute();
                }

                @Override
                public void credentialsCancelled() {
                    CredentialManager.getInstance().setDatabaseUsed(false);
                    handleAuthRequest(ctx, host, realm, l, latch);
                    latch.countDown();
                }
            });
        } else {
            new AsyncTask<Void, Void, Credential>() {
                @Override
                protected Credential doInBackground(Void... v) {
                    Credential c = null;
                    if (hasDatabase() && isDatabaseUsed()) {
                        c = mDb.credentialDao().get(host, realm);

                        if (c != null && mSendCreds.contains(c)) {
                            // password already has been used, but seems to be not correct
                            // remove from db, open dialog
                            mSendCreds.remove(c);
                            mDb.credentialDao().remove(c);
                            c = null;
                        }
                    }
                    return c;
                }

                @Override
                protected void onPostExecute(Credential c) {
                    if (c != null) {
                        mSendCreds.add(c);
                        l.credentialsEntered(c.getUser(), c.getPasswd());
                        latch.countDown();
                    } else {
                        // could not retrieve creds from store => ask user
                        UiUtil.showPasswordDialog(ctx, host, realm, new UiUtil.CredentialsListener() {
                            @Override
                            public void credentialsEntered(String host, String realm, String user, String password, boolean store) {
                                if (store) {
                                    new AsyncTask<Void, Void, Void>() {
                                        @Override
                                        protected Void doInBackground(Void... voids) {
                                            if (mDb != null) {
                                                mDb.credentialDao().insert(new Credential(host, realm, user, password));
                                            }
                                            l.credentialsEntered(user, password);
                                            latch.countDown();
                                            return null;
                                        }
                                    }.execute();
                                } else {
                                    l.credentialsEntered(user, password);
                                    latch.countDown();
                                }
                            }

                            @Override
                            public void credentialsCancelled() {
                                l.credentialsCancelled();
                                latch.countDown();
                            }
                        });
                    }
                }
            }.execute();
        }
    }

    public boolean hasDatabase() {
        return mDb != null;
    }

    public void setDatabaseUsed(boolean used) {
        mDatabaseUsed = used;
    }

    public boolean isDatabaseUsed() {
        return mDatabaseUsed;
    }

    public void getRestCredential(String host, String realm, CredentialsListener l) {
        mHandler.post(() -> {
            Credential c = null;
            if (mDb != null) {
                c = mDb.credentialDao().get(host, realm);
            }

            final Credential c2 = c;
            Handler handler = new Handler(Looper.getMainLooper());
            handler.post(() -> {
                if (c2 != null) {
                    l.credentialsEntered(c2.getUser(), c2.getPasswd());
                } else {
                    l.credentialsCancelled();
                }
            });
        });
    }


    public State getDatabaseState(Context ctx) {
        SQLiteDatabase.loadLibs(ctx);
        File dbPath = ctx.getDatabasePath("HPV");

        if (dbPath.exists()) {
            try (SQLiteDatabase db = SQLiteDatabase.openDatabase(dbPath.getAbsolutePath(), "",
                    null, SQLiteDatabase.OPEN_READONLY)) {
                db.getVersion();

                return (State.UNENCRYPTED);
            } catch (Exception e) {
                return (State.ENCRYPTED);
            }
        }

        return(State.DOES_NOT_EXIST);
    }

    public void openDb(Context ctx, String password) {
        RoomDatabase.Builder<AppDatabase> builder = Room.databaseBuilder(ctx, AppDatabase.class, "HPV");

        if (password != null) {
            final byte[] passphrase = SQLiteDatabase.getBytes(password.toCharArray());
            final SupportFactory factory = new SupportFactory(passphrase);
            builder.openHelperFactory(factory);
        }

        AppDatabase db = builder.build();

        // check if password is correct
        final State dbState = getDatabaseState(ctx);
        if (dbState == CredentialManager.State.ENCRYPTED) {
            try {
                db.credentialDao().get("a", "b");
                mDb = db;
            } catch (SQLiteException e) {
                db.close();
                // password wrong
            }
        } else {
            mDb = db;

            // insert a dummy record to make sure the database exists on subsequent starts
            if (dbState == State.DOES_NOT_EXIST) {
                mHandler.post(() -> {
                    if (mDb != null) {
                        mDb.credentialDao().insert(new Credential("nohost", "norealm", "nouser", "nopasswd"));
                    }
                });
            }
        }
    }

    public void encryptDb(Context ctx, String password) {
        final State dbState = getDatabaseState(ctx);

        if (dbState == CredentialManager.State.UNENCRYPTED) {
            if (mDb != null) {
                mDb.close();
                mDb = null;
            }

            AppDatabase db = Room.databaseBuilder(ctx, AppDatabase.class, "HPV").build();
            List<Credential> creds = db.credentialDao().getAll();
            db.close();

            ctx.deleteDatabase("HPV");

            openDb(ctx, password);
            mDb.credentialDao().insert(creds.toArray(new Credential[0]));
        }
    }

    public enum State {
        DOES_NOT_EXIST, UNENCRYPTED, ENCRYPTED
    }

    public interface CredentialsListener {
        void credentialsEntered(String user, String pass);
        void credentialsCancelled();
    }
} public class HelpActivity extends ScreenControllingActivity {
    private MenuItem mForumItem;
    private MenuItem mWikiItem;
    private MenuItem mFileItem;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_help);

        Toolbar myToolbar = findViewById(R.id.toolbar);
        setSupportActionBar(myToolbar);

        ActionBar ab = getSupportActionBar();
        if (ab != null) {
            ab.setDisplayHomeAsUpEnabled(true);
        }

        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext());
        String theme = prefs.getString(Constants.PREF_THEME, "dark");

        if ("dark".equals(theme)) {
            myToolbar.setPopupTheme(R.style.Theme_AppCompat_NoActionBar);
        } else {
            myToolbar.setPopupTheme(R.style.Theme_AppCompat_Light_NoActionBar);
        }

        showHelp();
    }

    private void showHelp() {
        if (mFileItem != null) {
            mFileItem.setEnabled(false);
        }

        if (mForumItem != null) {
            mForumItem.setEnabled(true);
        }

        if (mWikiItem != null) {
            mWikiItem.setEnabled(true);
        }

        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext());
        String theme = prefs.getString(Constants.PREF_THEME, "dark");

        loadMarkdownFromAssets(getString(R.string.helpFile), "dark".equals(theme));
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.help_toolbar_menu, menu);

        mForumItem = menu.findItem(R.id.action_goto_forum);
        mWikiItem = menu.findItem(R.id.action_goto_wiki);
        mFileItem = menu.findItem(R.id.action_show_help);

        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();

        if (id == R.id.action_goto_forum) {
            mFileItem.setEnabled(true);
            mWikiItem.setEnabled(true);
            mForumItem.setEnabled(false);

            final MarkdownView markdownView = findViewById(R.id.activity_help_webview);
            markdownView.loadUrl("https://community.openhab.org/t/habpanelviewer/34112/");
            return true;
        }

        if (id == R.id.action_goto_wiki) {
            mFileItem.setEnabled(true);
            mWikiItem.setEnabled(false);
            mForumItem.setEnabled(true);

            final MarkdownView markdownView = findViewById(R.id.activity_help_webview);
            markdownView.loadUrl("https://github.com/vbier/habpanelviewer/wiki");
            return true;
        }

        if (id == R.id.action_show_help) {
            showHelp();
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    private void loadMarkdownFromAssets(String assetsFilePath, boolean dark) {
        try {
            StringBuilder buf = new StringBuilder();
            if (dark) {
                buf.append("<style> body { background:black; color:white } a { color: lightblue; } </style>\n");
            }
            InputStream json = getAssets().open(assetsFilePath);
            BufferedReader in = new BufferedReader(new InputStreamReader(json, StandardCharsets.UTF_8));

            String str;
            while((str = in.readLine()) != null) {
                buf.append(str).append("\n");
            }

            in.close();

            final MarkdownView markdownView = findViewById(R.id.activity_help_webview);
            markdownView.setMarkDownText(buf.toString());
        } catch (IOException var6) {
            var6.printStackTrace();
        }
    }

    @Override
    public View getScreenOnView() {
        return findViewById(R.id.activity_help_webview);
    }
} public interface ISseConnectionListener extends ISseListener {
    void statusChanged(SseConnection.Status newStatus);
} public class ServerConnection implements IStatePropagator {

    private static final String TAG = "HPV-ServerConnection";

    private String mServerURL;
    private String mOHVersion;

    private final OpenhabSseConnection mSseConnection = new OpenhabSseConnection();
    private final RestClient mRestClient = new RestClient();

    private final CertificateManager.ICertChangedListener mCertListener;
    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent i) {
            final String item = i.getStringExtra(Constants.INTENT_FLAG_ITEM_NAME);
            final String state = i.getStringExtra(Constants.INTENT_FLAG_ITEM_STATE);
            final String timeoutState = i.getStringExtra(Constants.INTENT_FLAG_ITEM_TIMEOUT_STATE);
            final int timeout = i.getIntExtra(Constants.INTENT_FLAG_TIMEOUT, 60);

            updateStateWithTimeout(item, state, timeoutState, timeout);
        }
    };

    private final HashMap<String, ArrayList<IStateUpdateListener>> mSubscriptions = new HashMap<>();
    private final HashMap<String, ArrayList<IStateUpdateListener>> mCmdSubscriptions = new HashMap<>();
    private final HashMap<String, String> mValues = new HashMap<>();

    private final HashMap<String, String> lastUpdates = new HashMap<>();
    private final AveragePropagator averagePropagator = new AveragePropagator(this);

    public ServerConnection(Context context) {
        mCertListener = () -> {
            Log.d(TAG, "cert added, reconnecting to server...");

            if (mSseConnection.getStatus() == SseConnection.Status.CERTIFICATE_ERROR) {
                mSseConnection.connect();
            }
        };

        mSseConnection.addListener(new SseConnectionListener());
        mSseConnection.addItemValueListener(new SseStateUpdateListener());

        CertificateManager.getInstance().addCertListener(mCertListener);

        IntentFilter f = new IntentFilter();
        f.addAction(Constants.INTENT_ACTION_SET_WITH_TIMEOUT);

        LocalBroadcastManager.getInstance(context).registerReceiver(mReceiver, f);
    }

    public void addConnectionListener(ISseConnectionListener l) {
        mSseConnection.addListener(l);
    }

    public void subscribeCommandItem(IStateUpdateListener l, String name) {
        if (name != null && !"".equals(name) && checkItemsChanged(mCmdSubscriptions, l, false, name)) {
            String cmdItemName = mCmdSubscriptions.isEmpty() ? null : mCmdSubscriptions.keySet().iterator().next();
            mSseConnection.setItemNames(cmdItemName, mSubscriptions.keySet().toArray(new String[0]));
            reconnect();
        }
    }

    public void subscribeItems(IStateUpdateListener l, String... names) {
        if (checkItemsChanged(mSubscriptions, l, true, names)) {
            String cmdItemName = mCmdSubscriptions.isEmpty() ? null : mCmdSubscriptions.keySet().iterator().next();
            mSseConnection.setItemNames(cmdItemName, mSubscriptions.keySet().toArray(new String[0]));
            reconnect();
        }
    }

    private boolean checkItemsChanged(HashMap<String, ArrayList<IStateUpdateListener>> subscriptions,
                                      IStateUpdateListener l, boolean initialValue, String... names) {
        final HashSet<String> currentItems = new HashSet<>();
        final HashSet<String> newItems = new HashSet<>();
        final HashSet<String> removedItems = new HashSet<>();

        // remove empty names
        for (String name : names) {
            if (name != null && !name.trim().isEmpty()) {
                currentItems.add(name);
            }
        }

        boolean itemsChanged = false;
        synchronized (subscriptions) {
            for (String name : new HashSet<>(subscriptions.keySet())) {
                ArrayList<IStateUpdateListener> listeners = subscriptions.get(name);
                if (listeners != null && listeners.contains(l) && !currentItems.contains(name)) {
                    listeners.remove(l);

                    if (listeners.isEmpty()) {
                        itemsChanged = true;
                        subscriptions.remove(name);
                        mValues.remove(name);

                        removedItems.add(name);
                    }
                }
            }

            if (removedItems.size() > 0) {
                Log.d(TAG, "unsubscribing items: " + removedItems.toString());
            }

            for (String name : currentItems) {
                ArrayList<IStateUpdateListener> listeners = subscriptions.get(name);
                if (listeners == null) {
                    itemsChanged = true;
                    listeners = new ArrayList<>();
                    subscriptions.put(name, listeners);

                    newItems.add(name);
                } else if (mValues.containsKey(name) && initialValue) {
                    l.itemUpdated(name, mValues.get(name));
                }

                if (!listeners.contains(l)) {
                    listeners.add(l);
                }
            }

            if (newItems.size() > 0) {
                Log.d(TAG, "subscribing items: " + newItems.toString());
            }

            if (subscriptions.size() > 0 && newItems.size() + removedItems.size() > 0) {
                Log.v(TAG, "current set of items: " + subscriptions.keySet().toString());
            }
        }

        return itemsChanged;
    }

    public void updateFromPreferences(SharedPreferences prefs, Context ctx) {
        final boolean serverChanged = mServerURL == null
                || !mServerURL.equalsIgnoreCase(prefs.getString(Constants.PREF_SERVER_URL, ""));
        final boolean serverVersionChanged = mOHVersion == null
                || !mOHVersion.equalsIgnoreCase(prefs.getString(Constants.PREF_OH_VERSION, "OH3"));

        if (serverChanged || serverVersionChanged) {
            close();
        }

        if (serverVersionChanged) {
            mOHVersion = prefs.getString(Constants.PREF_OH_VERSION, "OH3");
            Log.d(TAG, "server version changed: " + mOHVersion);
        }

        if (serverChanged) {
            mServerURL = prefs.getString(Constants.PREF_SERVER_URL, "");
            Log.d(TAG, "new server URL: " + mServerURL);
        }

        if (serverChanged || serverVersionChanged) {
            mSseConnection.setServer(mServerURL, OpenhabSseConnection.OHVersion.valueOf(mOHVersion));
        }
    }

    private synchronized boolean isSseConnected() {
        return mSseConnection.getStatus() == SseConnection.Status.CONNECTED;
    }

    public synchronized void reconnect() {
        if (isSseConnected()) {
            mSseConnection.disconnect();
        }
        mSseConnection.connect();
    }

    private synchronized void close() {
        mSseConnection.disconnect();
    }

    public void terminate(Context context) {
        CertificateManager.getInstance().removeCertListener(mCertListener);
        LocalBroadcastManager.getInstance(context).unregisterReceiver(mReceiver);

        averagePropagator.terminate();
        mSseConnection.dispose();
        mSubscriptions.clear();
    }

    public String getState(String item) {
        return mValues.get(item);
    }

    public void removeFromAverage(String item) {
        averagePropagator.removeFromAverage(item);
    }

    public void addStateToAverage(String item, Float state, int updateInterval) {
        if (averagePropagator.addStateToAverage(item, state, updateInterval)) {
            // in case this is the first value for the item propagate it
            updateState(item, state.toString());
        }
    }

    private void updateStateWithTimeout(String item, String state, String timeoutState, int timeoutInSeconds) {
        updateState(item, state);

        averagePropagator.setStateIn(item, timeoutState, timeoutInSeconds);
    }

    public void updateState(String item, String state) {
        updateState(item, state, false);
    }

    private void updateState(String item, String state, boolean resend) {
        if (item != null && !item.isEmpty() && state != null && (resend || !state.equals(getState(item)))) {
            if (resend || !state.equals(lastUpdates.get(item))) {
                synchronized (lastUpdates) {
                    lastUpdates.put(item, state);
                }

                if (isSseConnected()) {
                    Log.v(TAG, "Sending state update for " + item + ": " + state);
                    mRestClient.setItemState(mServerURL, new ItemState(item, state));
                }
            }
        }
    }

    public void updateJpeg(String item, byte[] data) {
        if (data != null) {
            updateState(item, "data:image/jpeg;base64," + Base64.encodeToString(data, Base64.NO_WRAP));
        }
    }

    public void sendCurrentValues() {
        Log.v(TAG, "Sending pending updates...");
        synchronized (lastUpdates) {
            for (Map.Entry<String, String> entry : lastUpdates.entrySet()) {
                updateState(entry.getKey(), entry.getValue(), true);
            }
        }
        Log.v(TAG, "Pending updates sent");
    }

    public SseConnection getSseConnection() {
        return mSseConnection;
    }

    private void propagateItem(String name, String value) {
        if (!value.equals(mValues.get(name))) {
            propagate(mSubscriptions, name, value);
        }
    }

    private void propagateCommand(String name, String value) {
        propagate(mCmdSubscriptions, name, value);
    }

    private void propagate(HashMap<String, ArrayList<IStateUpdateListener>> subscriptions, String name, String value) {
        mValues.put(name, value);

        Log.v(TAG, "propagating item: " + name + "=" + value);

        final ArrayList<IStateUpdateListener> listeners;
        synchronized (subscriptions) {
            listeners = subscriptions.get(name);
        }

        if (listeners != null) {
            for (IStateUpdateListener l : listeners) {
                l.itemUpdated(name, value);
            }
        }
    }

    private class SseStateUpdateListener implements IStateUpdateListener {
        @Override
        public void itemUpdated(String name, String value) {
            if (mSubscriptions.containsKey(name)) {
                propagateItem(name, value);
            }
            if (mCmdSubscriptions.containsKey(name)) {
                propagateCommand(name, value);
            }
        }
    }

    private class SseConnectionListener implements ISseConnectionListener {
        private SseConnection.Status mLastStatus;

        private final ISubscriptionListener mListener = new ISubscriptionListener() {
            @Override
            public void itemUpdated(String name, String value) {
                propagateItem(name, value);
            }

            @Override
            public void itemInvalid(String name) {
                propagateItem(name, "ITEM NOT FOUND");
            }
        };

        @Override
        public void statusChanged(SseConnection.Status newStatus) {
            if (newStatus == SseConnection.Status.CONNECTED && mLastStatus != SseConnection.Status.CONNECTED) {
                sendCurrentValues();
                fetchCurrentItemsState();
            } else if (mLastStatus == SseConnection.Status.CONNECTED && newStatus != SseConnection.Status.CONNECTED) {
                mValues.clear();
                averagePropagator.clear();
            }
            mLastStatus = newStatus;
        }

        private synchronized void fetchCurrentItemsState() {
            HashSet<String> missingItems = new HashSet<>();
            synchronized (mSubscriptions) {
                for (String item : mSubscriptions.keySet()) {
                    if (!mValues.containsKey(item)) {
                        missingItems.add(item);
                    }
                }
            }

            Log.d(TAG, "Actively fetching items state");
            for (String item : missingItems) {
                mRestClient.getItemState(mServerURL, mListener, item);
            }
        }
    }
} public class SseConnection implements NetworkTracker.INetworkListener, CredentialManager.CredentialsListener {
    private static final String TAG = "HPV-SseConnection";

    String mUrl;

    private boolean mNetworkConnected;

    private final List<ISseListener> mListeners = new ArrayList<>();
    volatile Status mStatus = Status.NOT_CONNECTED;
    private EventSource mEventSource;

    SseConnection() {
    }

    void setServerUrl(String url) {
        if (mUrl == null && url != null || mUrl != null && !mUrl.equals(url)) {
            mUrl = url;

            if (mStatus.isConnecting() || mStatus == Status.CONNECTED) {
                disconnect();
            }

            if (mStatus == Status.NOT_CONNECTED || mStatus == Status.URL_MISSING) {
                connect();
            }
        }
    }

    @Override
    public void credentialsEntered(String user, String pass) {
        if (mStatus.isConnecting() || mStatus == Status.CONNECTED) {
            disconnect();
        }

        if (mStatus == Status.NOT_CONNECTED || mStatus == Status.UNAUTHORIZED) {
            connect();
        }
    }

    @Override
    public void credentialsCancelled() {
    }

    void addListener(ISseListener l) {
        synchronized (mListeners) {
            if (!mListeners.contains(l)) {
                mListeners.add(l);
            }
        }
    }

    void removeListener(ISseListener l) {
        synchronized (mListeners) {
            mListeners.remove(l);
        }
    }

    @Override
    public void disconnected() {
        setNetworkConnected(false);
    }

    @Override
    public void connected() {
        setNetworkConnected(true);
    }

    synchronized void connect() {
        Log.v(TAG, "SseConnection.connect");
        Log.v(TAG, "mEventSource=" + (mEventSource == null ? "null" : mEventSource.hashCode()));

        if (mEventSource != null) {
            EventSource oldSource = mEventSource;
            mEventSource = null;
            oldSource.cancel();
        }

        if (!mNetworkConnected) {
            setStatus(Status.NO_NETWORK);
        } else if (mUrl == null || mUrl.trim().isEmpty()) {
            setStatus(Status.URL_MISSING);
        } else {
            setStatus(Status.CONNECTING);

            OkHttpClient client = createConnection();
            EventSourceListener sseHandler = new SSEHandler();
            try {
                Request request = new Request.Builder().url(buildUrl()).build();
                mEventSource = EventSources.createFactory(client).newEventSource(request, sseHandler);

                Log.v(TAG, "mEventSource = " + mEventSource.request().url().toString());
                Log.v(TAG, "mEventSource=" + (mEventSource == null ? "null" : mEventSource.hashCode()));
            } catch (IllegalArgumentException e) {
                setStatus(Status.INVALID_URL);
            }
        }
    }

    OkHttpClient createConnection() {
        return OkHttpClientFactory.getInstance().create();
    }

    String buildUrl() {
        return mUrl + "/rest/events";
    }

    synchronized void disconnect() {
        if (mEventSource != null) {
            EventSource oldSource = mEventSource;
            mEventSource = null;
            oldSource.cancel();
        }

        setStatus(Status.NOT_CONNECTED);
    }

    private void setNetworkConnected(boolean connected) {
        mNetworkConnected = connected;

        if (!connected) {
            disconnect();
            setStatus(Status.NO_NETWORK);
        } else if (mStatus == Status.NOT_CONNECTED || mStatus == Status.NO_NETWORK) {
            connect();
        }
    }

    private void setStatus(Status status) {
        Log.e(TAG, "setting status " + status.name() + ", connection=" + (mEventSource == null ? "null" : mEventSource.hashCode()), new Exception("dummy"));

        if (status != mStatus) {
            mStatus = status;

            synchronized (mListeners) {
                for (ISseListener l : mListeners) {
                    if (l instanceof ISseConnectionListener) {
                        ((ISseConnectionListener) l).statusChanged(status);
                    }
                }
            }
        }
    }

    public Status getStatus() {
        return mStatus;
    }

    void dispose() {
        disconnect();
        mListeners.clear();
    }

    interface ISseDataListener extends ISseListener{
        void data(String data);
    }

    public enum Status {
        // initial state
        NOT_CONNECTED,
        CONNECTING, CONNECTED,
        // device has not network
        NO_NETWORK,
        // url is not set or invalid
        URL_MISSING, INVALID_URL,
        // missing or wrong auth
        UNAUTHORIZED,
        // failed to connect due to invalid certificate
        CERTIFICATE_ERROR,
        // other failure
        FAILURE;

        boolean isConnecting() {
            return this == CONNECTING;
        }
    }

    public void postDelayed(Runnable r, long delay) {
        final Handler handler = new Handler(Looper.getMainLooper());
        handler.postDelayed(SseConnection.this::connect, delay);
    }

    private class SSEHandler extends EventSourceListener {
        private int failureCount = 0;
        SSEHandler() { }

        @Override
        public void onOpen(@NotNull EventSource eventSource, @NotNull Response response) {
            Log.v(TAG, "SSEHandler.onOpen: mEventSource=" + (mEventSource == null ? "null" : mEventSource.hashCode()));

            setStatus(Status.CONNECTED);
        }

        @Override
        public void onClosed(@NotNull EventSource eventSource) {
            if (mStatus == Status.CONNECTED){
                setStatus(Status.NOT_CONNECTED);

                if (mEventSource != null) {
                    triggerReconnect();
                }
            }
        }

        @Override
        public void onEvent(@NotNull EventSource eventSource, @Nullable String id, @Nullable String type, @NotNull String data) {
            if ("message".equals(type)) {
                failureCount = 0;

                synchronized (mListeners) {
                    for (ISseListener l : mListeners) {
                        if (l instanceof ISseDataListener) {
                            ((ISseDataListener) l).data(data);
                        }
                    }
                }
            }
        }

        @Override
        public void onFailure(@NotNull EventSource eventSource, @Nullable Throwable t, @Nullable Response response) {
            if (t instanceof SSLHandshakeException) {
                setStatus(Status.CERTIFICATE_ERROR);
            } else if (response != null && (response.code() == 401 || response.code() == 407)) {
                setStatus(Status.UNAUTHORIZED);
            } else {
                setStatus(Status.FAILURE);

                triggerReconnect();
            }
        }

        private void triggerReconnect() {
            failureCount++;

            int delay = 500;

            if (failureCount > 100) {
                delay = 5000;
            } else if (failureCount > 50) {
                delay = 2000;
            } else if (failureCount > 30) {
                delay = 1000;
            } else if (failureCount > 10) {
                delay = 1000;
            }

            postDelayed(SseConnection.this::connect, delay);
        }
    }
} public class PreferenceActivity extends ScreenControllingActivity implements PreferenceCallback {
    private static final String TAG_NESTED = "TAG_NESTED";

    private Toolbar mToolbar;
    private MenuItem mUpItem;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_preferences);

        mToolbar = findViewById(R.id.toolbar);
        setSupportActionBar(mToolbar);
        ActionBar ab = getSupportActionBar();
        if (ab != null) {
            ab.setDisplayHomeAsUpEnabled(true);
        }

        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext());
        String theme = prefs.getString(Constants.PREF_THEME, "dark");

        if ("dark".equals(theme)) {
            mToolbar.setPopupTheme(R.style.ThemeOverlay_AppCompat_Dark);
        } else {
            mToolbar.setPopupTheme(R.style.ThemeOverlay_AppCompat_Light);
        }

        if (savedInstanceState == null) {
            PreferencesMain mPrefFragment = new PreferencesMain();
            mPrefFragment.setArguments(getIntent().getExtras());

            getFragmentManager().beginTransaction().add(R.id.preferences_fragment_container, mPrefFragment).commit();
        }
    }

    @Override
    public void onBackPressed() {
        // this if statement is necessary to navigate through nested and main fragments
        if (getFragmentManager().getBackStackEntryCount() == 0) {
            super.onBackPressed();
        } else {
            mUpItem.setEnabled(getFragmentManager().getBackStackEntryCount() > 1);
            getFragmentManager().popBackStack();
        }
    }

    @Override
    public void onNestedPreferenceSelected(String id) {
        mUpItem.setEnabled(true);
        getFragmentManager().beginTransaction().replace(R.id.preferences_fragment_container,
                PreferenceFragment.newInstance(id, getIntent().getExtras()), TAG_NESTED).addToBackStack(TAG_NESTED).commit();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.preferences_toolbar_menu, menu);
        mUpItem = menu.findItem(R.id.action_back);

        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();

        if (id == R.id.action_pref_export) {
            if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
                    != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(this,
                        new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
                        Constants.REQUEST_WRITE_EXTERNAL_STORAGE);
            } else {
                PreferenceUtil.saveSharedPreferencesToFile(this, mToolbar);
            }

            return true;
        }

        if (id == R.id.action_pref_import) {
            if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
                    != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(this,
                        new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
                        Constants.REQUEST_READ_EXTERNAL_STORAGE);
            } else {
                PreferenceUtil.loadSharedPreferencesFromFile(this, mToolbar);
            }
            return true;
        }

        if (id == R.id.action_back) {
            onBackPressed();
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode,
                                           @NonNull String[] permissions,
                                           @NonNull int[] grantResults) {
        switch (requestCode) {
            case Constants.REQUEST_READ_EXTERNAL_STORAGE: {
                if (grantResults.length > 0
                        && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    PreferenceUtil.loadSharedPreferencesFromFile(this, mToolbar);
                }
            }
            case Constants.REQUEST_WRITE_EXTERNAL_STORAGE: {
                if (grantResults.length > 0
                        && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    PreferenceUtil.saveSharedPreferencesToFile(this, mToolbar);
                }
            }
        }
    }

    @Override
    public View getScreenOnView() {
        return mToolbar;
    }
} public class AccelerometerMonitor extends AbstractDeviceMonitor {
    private static final String TAG = "HPV-AccelerometerMon";

    private float mSensitivity = 1f;
    private String mSensitivityStr = "1.0";
    private long mLastMotionTime;
    private Boolean mMotion = Boolean.FALSE;

    public AccelerometerMonitor(Context ctx, SensorManager sensorManager, ServerConnection serverConnection) {
        super(ctx, sensorManager, serverConnection, ctx.getString(R.string.pref_accelerometer), "accelerometer", Sensor.TYPE_LINEAR_ACCELERATION);
    }

    @Override
    public void onSensorChanged(SensorEvent event) {
        // Shake detection
        float x = event.values[0];
        float y = event.values[1];
        float z = event.values[2];
        //Log.v(TAG, "onSensorChanged: x=" + x + ", y=" + y + ",z=" + z);

        float mAccel = (float) Math.sqrt(x * x + y * y + z * z);

        //Log.v(TAG, "onSensorChanged: mAccel=" + mAccel);
        if (mAccel > mSensitivity) {
            mLastMotionTime = System.currentTimeMillis();
            if (!mMotion) {
                mMotion = true;
                mServerConnection.updateState(mSensorItem, "CLOSED");
            }
        } else {
            if (mMotion && System.currentTimeMillis() - mLastMotionTime > 60000) {
                mMotion = false;
                mServerConnection.updateState(mSensorItem, "OPEN");
            }
        }
    }

    protected synchronized void addStatusItems(ApplicationStatus status) {
        if (mSensorEnabled) {
            String state = mCtx.getString(R.string.enabled);
            if (!mSensorItem.isEmpty()) {
                state += "\n" + mCtx.getString(R.string.deviceMoving, mMotion, mSensorItem, mSensorState);
            }
            state += "\n" + mCtx.getString(R.string.pref_accelerometerSensitivity) + ": " + sensText();

            status.set(mCtx.getString(R.string.pref_accelerometer), state);
        } else {
            status.set(mCtx.getString(R.string.pref_accelerometer), mCtx.getString(R.string.disabled));
        }
    }

    private String sensText() {
        String[] sensVals = mCtx.getResources().getStringArray(R.array.sensitivityValues);
        String[] sensitivities = mCtx.getResources().getStringArray(R.array.sensitivityNames);

        int idx = Arrays.asList(sensVals).indexOf(mSensitivityStr);
        return idx == -1 ? "??" : sensitivities[idx];
    }

    public synchronized void updateFromPreferences(SharedPreferences prefs) {
        super.updateFromPreferences(prefs);

        mSensitivityStr = prefs.getString(Constants.PREF_PREFIX + mPreferenceKey + Constants.PREF_SUFFIX_SENSITIVITY, "3.0");
        mSensitivity = Float.parseFloat(mSensitivityStr);
    }
} public class BatteryMonitor implements IDeviceMonitor, IStateUpdateListener {
    private static final String TAG = "HPV-BatteryMonitor";

    private final Context mCtx;
    private final ServerConnection mServerConnection;

    private final BroadcastReceiver mBatteryReceiver;
    private boolean mBatteryEnabled;

    private String mBatteryLowItem;
    private String mBatteryChargingItem;
    private String mBatteryLevelItem;
    private String mBatteryTempItem;

    private boolean mBatteryLow;
    private boolean mBatteryCharging;
    private Integer mBatteryLevel;
    private Float mBatteryTemp;
    private String mBatteryLowState;
    private String mBatteryChargingState;
    private String mBatteryLevelState;
    private String mBatteryTempState;
    private final IntentFilter mIntentFilter;

    public BatteryMonitor(Context context, ServerConnection serverConnection) {
        mCtx = context;
        mServerConnection = serverConnection;

        EventBus.getDefault().register(this);

        mBatteryReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                if (Intent.ACTION_BATTERY_LOW.equals(intent.getAction())
                        || Intent.ACTION_BATTERY_OKAY.equals(intent.getAction())) {
                    mBatteryLow = Intent.ACTION_BATTERY_LOW.equals(intent.getAction());
                    mServerConnection.updateState(mBatteryLowItem, mBatteryLow ? "CLOSED" : "OPEN");
                } else if (Intent.ACTION_BATTERY_CHANGED.equals(intent.getAction())) {
                    float temp = intent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE,0);
                    mBatteryTemp = temp / 10;
                    mServerConnection.updateState(mBatteryTempItem, String.valueOf(mBatteryTemp));
                    int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
                    int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
                    mBatteryLevel = (int) ((level / (float) scale) * 100);
                    mServerConnection.updateState(mBatteryLevelItem, String.valueOf(mBatteryLevel));
                } else {
                    mBatteryCharging = Intent.ACTION_POWER_CONNECTED.equals(intent.getAction());
                    mServerConnection.updateState(mBatteryChargingItem, mBatteryCharging ? "CLOSED" : "OPEN");
                }
            }
        };

        mIntentFilter = new IntentFilter();
        mIntentFilter.addAction(Intent.ACTION_POWER_CONNECTED);
        mIntentFilter.addAction(Intent.ACTION_POWER_DISCONNECTED);
        mIntentFilter.addAction(Intent.ACTION_BATTERY_LOW);
        mIntentFilter.addAction(Intent.ACTION_BATTERY_OKAY);
        mIntentFilter.addAction(Intent.ACTION_BATTERY_CHANGED);
    }

    @Override
    public void disablePreferences(Intent intent) { }

    @Override
    public synchronized void terminate() {
        EventBus.getDefault().unregister(this);
        try {
            mCtx.unregisterReceiver(mBatteryReceiver);
            Log.d(TAG, "unregistering battery receiver...");
        } catch (IllegalArgumentException e) {
            // not registered
        }
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onMessageEvent(ApplicationStatus status) {
        if (mBatteryEnabled) {
            String state = mCtx.getString(R.string.enabled);
            if (!mBatteryLowItem.isEmpty()) {
                state += "\n" + mCtx.getString(R.string.battLow, mBatteryLow, mBatteryLowItem, mBatteryLowState);
            }
            if (!mBatteryChargingItem.isEmpty()) {
                state += "\n" + mCtx.getString(R.string.battCharging, mBatteryCharging, mBatteryChargingItem, mBatteryChargingState);
            }
            if (!mBatteryLevelItem.isEmpty()) {
                state += "\n" + mCtx.getString(R.string.battLevel, mBatteryLevel, mBatteryLevelItem, mBatteryLevelState);
            }
            if (!mBatteryTempItem.isEmpty()) {
                state += "\n" + mCtx.getString(R.string.battTemp, mBatteryTemp, mBatteryTempItem, mBatteryTempState);
            }
            status.set(mCtx.getString(R.string.pref_battery), state);
        } else {
            status.set(mCtx.getString(R.string.pref_battery), mCtx.getString(R.string.disabled));
        }
    }

    @Override
    public synchronized void updateFromPreferences(SharedPreferences prefs) {
        if (mBatteryEnabled != prefs.getBoolean(Constants.PREF_BATTERY_ENABLED, false)) {
            mBatteryEnabled = !mBatteryEnabled;

            if (mBatteryEnabled) {
                Log.d(TAG, "registering battery receiver...");
                mCtx.registerReceiver(mBatteryReceiver, mIntentFilter);
            } else {
                Log.d(TAG, "unregistering battery receiver...");
                mCtx.unregisterReceiver(mBatteryReceiver);
            }
        }

        mBatteryLowItem = prefs.getString(Constants.PREF_BATTERY_ITEM, "");
        mBatteryChargingItem = prefs.getString(Constants.PREF_BATTERY_CHARGING_ITEM, "");
        mBatteryLevelItem = prefs.getString(Constants.PREF_BATTERY_LEVEL_ITEM, "");
        mBatteryTempItem = prefs.getString(Constants.PREF_BATTERY_TEMP_ITEM, "");

        mServerConnection.subscribeItems(this, mBatteryLowItem, mBatteryChargingItem, mBatteryLevelItem, mBatteryTempItem);
    }

    @Override
    public void itemUpdated(String name, String value) {
        if (name.equals(mBatteryChargingItem)) {
            mBatteryChargingState = value;
        } else if (name.equals(mBatteryLevelItem)) {
            mBatteryLevelState = value;
        } else if (name.equals(mBatteryLowItem)) {
            mBatteryLowState = value;
        } else if (name.equals(mBatteryTempItem)) {
            mBatteryTempState = value;
        }
    }
} public class BrightnessMonitor extends AbstractAveragingDeviceMonitor {
    public BrightnessMonitor(Context ctx, SensorManager sensorManager, ServerConnection serverConnection) {
        super(ctx, sensorManager, serverConnection, ctx.getString(R.string.pref_brightness), "brightness", Sensor.TYPE_LIGHT);
    }

    @Override
    String getInfoString(Float value, String item, String state) {
        return mCtx.getString(R.string.brightness, mValue, mSensorItem, mSensorState);
    }
} public class ConnectedIndicator implements IStateUpdateListener {
    private static final String TAG = "HPV-ConnectedIndicator";

    @SuppressLint("SimpleDateFormat")
    private final SimpleDateFormat mFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");

    private boolean mStartEnabled;
    private boolean mEnabled;

    private String mStartStatusItem;
    private String mStatusItem;
    private int mInterval;

    private final Context mCtx;
    private final ServerConnection mServerConnection;
    private ConnectedReportingThread mReportConnection;

    private String mStatus;
    private long mStartTime = -1;
    private String mStatusState;
    private String mStartStatusState;

    public ConnectedIndicator(Context ctx, ServerConnection serverConnection) {
        mCtx = ctx;
        mServerConnection = serverConnection;

        EventBus.getDefault().register(this);
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onMessageEvent(ApplicationStatus status) {
        if (mEnabled) {
            String state = mCtx.getString(R.string.enabled);

            Resources res = mCtx.getResources();
            state += "\n" + res.getQuantityString(R.plurals.updateInterval, mInterval, mInterval);

            if (!mStatusItem.isEmpty()) {
                try {
                    Date date = mFormat.parse(mStatus);
                    state += "\n" + SimpleDateFormat.getDateTimeInstance().format(date)
                            + " [" + mStatusItem + "=" + mStatusState + "]";
                } catch (ParseException e) {
                    state += "\n???" + " [" + mStatusItem + "=" + mStatusState + "]";
                }
            }

            status.set(mCtx.getString(R.string.connectedIndicator), state);
        } else {
            status.set(mCtx.getString(R.string.connectedIndicator), mCtx.getString(R.string.disabled));
        }

        if (mStartEnabled) {
            String state = mCtx.getString(R.string.enabled);

            if (!mStartStatusItem.isEmpty()) {
                state += "\n" + (mStartTime == -1 ? "-" : SimpleDateFormat.getDateTimeInstance().format(new Date(mStartTime)))
                        + " [" + mStartStatusItem + "=" + mStartStatusState + "]";
            }

            status.set(mCtx.getString(R.string.startupIndicator), state);
        } else {
            status.set(mCtx.getString(R.string.startupIndicator), mCtx.getString(R.string.disabled));
        }
    }

    public synchronized void updateFromPreferences(SharedPreferences prefs) {
        int interval;
        try {
            interval = Integer.parseInt(prefs.getString(Constants.PREF_CONNECTED_INTERVAL, "60"));
        } catch (NumberFormatException e) {
            Log.e(TAG, "preferences contain invalid connected interval: "
                    + prefs.getString(Constants.PREF_CONNECTED_INTERVAL, "60"));
            interval = 60;
        }
        boolean intervalChanged = mInterval != interval;

        if (intervalChanged) {
            mInterval = interval;
        }

        boolean started = false;
        if (mEnabled != prefs.getBoolean(Constants.PREF_CONNECTED_ENABLED, false)) {
            mEnabled = !mEnabled;

            if (!mEnabled) {
                terminate();
            }

            if (mEnabled && mReportConnection == null) {
                mReportConnection = new ConnectedReportingThread();
                started = true;
            }
        }

        mStartEnabled = prefs.getBoolean(Constants.PREF_STARTUP_ENABLED, false);
        mStartStatusItem = prefs.getString(Constants.PREF_STARTUP_ITEM, "");
        mStatusItem = prefs.getString(Constants.PREF_CONNECTED_ITEM, "");

        if (mStartEnabled && mStartTime == -1) {
            mStartTime = System.currentTimeMillis();
            mServerConnection.updateState(mStartStatusItem, mFormat.format(new Date(mStartTime)));
        }

        if (intervalChanged && !started && mReportConnection != null) {
            mReportConnection.reportNow();
        }

        mServerConnection.subscribeItems(this, mStatusItem, mStartStatusItem);
    }

    @Override
    public void itemUpdated(String name, String value) {
        if (name.equals(mStatusItem)) {
            mStatusState = value;
        } else if (name.equals(mStartStatusItem)) {
            mStartStatusState = value;
        }
    }

    public synchronized void terminate() {
        EventBus.getDefault().unregister(this);

        if (mReportConnection != null) {
            mReportConnection.stopReporting();
            mReportConnection = null;
        }
    }

    private class ConnectedReportingThread extends Thread {
        private final AtomicBoolean fRunning = new AtomicBoolean(true);

        ConnectedReportingThread() {
            super("ConnectedReportingThread");
            setDaemon(true);
            start();
        }

        void stopReporting() {
            synchronized (fRunning) {
                fRunning.set(false);
                fRunning.notifyAll();
            }
        }

        void reportNow() {
            synchronized (fRunning) {
                fRunning.notifyAll();
            }
        }

        @Override
        public void run() {
            while (fRunning.get()) {
                mStatus = mFormat.format(new Date());
                mServerConnection.updateState(mStatusItem, mStatus);

                synchronized (fRunning) {
                    try {
                        fRunning.wait(mInterval * 1000);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            }
        }
    }
} public class DockingStateMonitor implements IDeviceMonitor, IStateUpdateListener {
    private static final String TAG = "HPV-DockingStateMonitor";

    private final Context mCtx;
    private final ServerConnection mServerConnection;

    private final BroadcastReceiver mDockStateReceiver;
    private boolean mDockStateEnabled;

    private String mDockStateItem;

    private boolean mDocked;
    private String mDockedState;

    private final IntentFilter mIntentFilter;

    public DockingStateMonitor(Context context, ServerConnection serverConnection) {
        mCtx = context;
        mServerConnection = serverConnection;

        EventBus.getDefault().register(this);

        mDockStateReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent dockStatus) {
                Log.d(TAG, "onReceive: dockStatus=" + dockStatus);

                int dockState = dockStatus.getIntExtra(Intent.EXTRA_DOCK_STATE, -1);
                Log.d(TAG, "onReceive: dockState=" + dockStatus);

                mDocked = dockState != Intent.EXTRA_DOCK_STATE_UNDOCKED;

                mServerConnection.updateState(mDockStateItem, mDocked ? "CLOSED" : "OPEN");
            }
        };

        mIntentFilter = new IntentFilter();
        mIntentFilter.addAction(Intent.ACTION_DOCK_EVENT);
    }

    @Override
    public void disablePreferences(Intent intent) { }

    @Override
    public synchronized void terminate() {
        EventBus.getDefault().unregister(this);
        try {
            mCtx.unregisterReceiver(mDockStateReceiver);
            Log.d(TAG, "unregistering docking state receiver...");
        } catch (IllegalArgumentException e) {
            // not registered
        }
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onMessageEvent(ApplicationStatus status) {
        if (mDockStateEnabled) {
            String state = mCtx.getString(R.string.enabled);
            if (!mDockStateItem.isEmpty()) {
                state += "\n" + mCtx.getString(R.string.docked, mDocked, mDockStateItem, mDockedState);
            }
            status.set(mCtx.getString(R.string.pref_dockingState), state);
        } else {
            status.set(mCtx.getString(R.string.pref_dockingState), mCtx.getString(R.string.disabled));
        }
    }

    @Override
    public synchronized void updateFromPreferences(SharedPreferences prefs) {
        mDockStateItem = prefs.getString(Constants.PREF_DOCKING_STATE_ITEM, "");

        if (mDockStateEnabled != prefs.getBoolean(Constants.PREF_DOCKING_STATE_ENABLED, false)) {
            mDockStateEnabled = !mDockStateEnabled;

            if (mDockStateEnabled) {
                Log.d(TAG, "registering docking state receiver...");
                mCtx.registerReceiver(mDockStateReceiver, mIntentFilter);

                Intent dockStatus = mCtx.registerReceiver(null, mIntentFilter);
                Log.d(TAG, "updateFromPreferences: dockStatus=" + dockStatus);

                int dockState = (dockStatus == null ? Intent.EXTRA_DOCK_STATE_UNDOCKED :
                        dockStatus.getIntExtra(Intent.EXTRA_DOCK_STATE, -1));
                Log.d(TAG, "updateFromPreferences: dockState=" + dockStatus);

                mDocked = dockState != Intent.EXTRA_DOCK_STATE_UNDOCKED;
                mServerConnection.updateState(mDockStateItem, mDocked ? "CLOSED" : "OPEN");
            } else {
                Log.d(TAG, "unregistering docking state receiver...");
                mCtx.unregisterReceiver(mDockStateReceiver);
            }
        }

        mServerConnection.subscribeItems(this, mDockStateItem);
    }

    @Override
    public void itemUpdated(String name, String value) {
        if (name.equals(mDockStateItem)) {
            mDockedState = value;
        }
    }
} public interface IDeviceMonitor {
    void terminate();
    void updateFromPreferences(SharedPreferences prefs);
    void disablePreferences(Intent intent);
} public class NoiseLevelMonitor implements IDeviceMonitor, IStateUpdateListener {
    private static final String TAG = "HPV-MicMonitor";

    private final Context mCtx;
    private final ServerConnection mServerConnection;
    private ReportingThread mReporter = new ReportingThread();

    private boolean mNoiseLevelEnabled;
    private String mNoiseLevelItem;
    private Float mNoiseLevel;
    private String mNoiseLevelState;
    private int mNoiseLevelInterval;

    public NoiseLevelMonitor(Context context, ServerConnection serverConnection) {
        mCtx = context;
        mServerConnection = serverConnection;

        EventBus.getDefault().register(this);
    }

    @Override
    public void disablePreferences(Intent intent) { }

    @Override
    public synchronized void terminate() {
        EventBus.getDefault().unregister(this);

        if (mReporter != null) {
            mReporter.shutdown();
        }
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onMessageEvent(ApplicationStatus status) {
        if (mNoiseLevelEnabled) {
            String state = mCtx.getString(R.string.enabled);
            if (!mNoiseLevelItem.isEmpty()) {
                state += "\n" + mCtx.getString(R.string.noiseLevel, mNoiseLevel, mNoiseLevelItem, mNoiseLevelState);
            }
            status.set(mCtx.getString(R.string.pref_noiseLevel), state);
        } else {
            status.set(mCtx.getString(R.string.pref_noiseLevel), mCtx.getString(R.string.disabled));
        }
    }

    @Override
    public synchronized void updateFromPreferences(SharedPreferences prefs) {
        if (mNoiseLevelEnabled != prefs.getBoolean(Constants.PREF_NOISE_LEVEL_ENABLED, false)) {
            mNoiseLevelEnabled = !mNoiseLevelEnabled;

            if (mNoiseLevelEnabled) {
                Log.d(TAG, "starting media recorder...");

                mReporter.report();
            } else {
                Log.d(TAG, "stopping media recorder...");
                mReporter.stopReporting();
            }
        }

        mNoiseLevelItem = prefs.getString(Constants.PREF_NOISE_LEVEL_ITEM, "");
        mNoiseLevelInterval = Integer.parseInt(prefs.getString(Constants.PREF_NOISE_LEVEL_INTERVALL, "5"));

        mServerConnection.subscribeItems(this, mNoiseLevelItem);
    }



    @Override
    public void itemUpdated(String name, String value) {
        if (name.equals(mNoiseLevelItem)) {
            mNoiseLevelState = value;
        }
    }

    private class ReportingThread extends Thread {
        private MediaRecorder mRecorder;
        private AtomicBoolean isRunning = new AtomicBoolean(false);
        private AtomicBoolean isTerminated = new AtomicBoolean(false);

        public ReportingThread() {
            MediaRecorder mRecorder = new MediaRecorder();
        }

        @Override
        public void run() {
            while (!isTerminated.get()) {
                synchronized (isRunning) {
                    try {
                        if (!isRunning.get() && !isTerminated.get()) {
                            // wait until started
                            isRunning.wait();
                        }

                        // whe we get here we are either running or have been terminated
                        while (isRunning.get()) {
                            try {
                                isRunning.wait(mNoiseLevelInterval * 1000);
                            } catch (InterruptedException e) {
                                // okay
                                Thread.currentThread().interrupt();
                            }

                            mNoiseLevel = (float) (20 * Math.log10(mRecorder.getMaxAmplitude() * 0.96518));
                            mServerConnection.updateState(mNoiseLevelItem, String.valueOf(mNoiseLevel));
                        }
                        // we only get here once we have been stopped
                    } catch (InterruptedException e) {
                        // okay
                        Thread.currentThread().interrupt();
                    }
                }
            }
        }

        public void report() {
            synchronized (isRunning) {
                if (!isRunning.getAndSet(true)) {
                    mRecorder = new MediaRecorder();
                    mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
                    mRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
                    mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
                    mRecorder.setOutputFile("/dev/null");

                    try {
                        mRecorder.prepare();
                        mRecorder.start();

                        start();
                        isRunning.notifyAll();
                    } catch (IOException | RuntimeException e) {
                        Log.e(TAG, "Failed to prepare Media Recorder", e);
                        stopReporting();
                    }
                }
            }
        }

        public void stopReporting() {
            synchronized (isRunning) {
                if (isRunning.getAndSet(false)) {
                    try {
                        mRecorder.stop();
                    } catch (IllegalStateException e) {
                        // ignore
                    }
                    mRecorder.reset();
                    mRecorder.release();
                    mRecorder = null;
                    isRunning.set(false);
                }
                isRunning.notifyAll();
            }
        }

        public void shutdown() {
            isTerminated.set(true);
            stopReporting();
        }
    }
} public class PressureMonitor extends AbstractAveragingDeviceMonitor {
    public PressureMonitor(Context ctx, SensorManager sensorManager, ServerConnection serverConnection) {
        super(ctx, sensorManager, serverConnection, ctx.getString(R.string.pref_pressure), "pressure", Sensor.TYPE_PRESSURE);
    }

    @Override
    String getInfoString(Float value, String item, String state) {
        return mCtx.getString(R.string.pressure, mValue, mSensorItem, mSensorState);
    }
} public class ProximityMonitor extends AbstractDeviceMonitor {
    private static final String TAG = "HPV-ProximityMonitor";

    private Boolean mProximity;

    public ProximityMonitor(Context ctx, SensorManager sensorManager, ServerConnection serverConnection) {
        super(ctx, sensorManager, serverConnection, ctx.getString(R.string.pref_proximity), "proximity", Sensor.TYPE_PROXIMITY);
    }

    @Override
    public void onSensorChanged(SensorEvent event) {
        float distance = event.values[0];

        Log.v(TAG, "onSensorChanged" + distance);

        if (distance < mSensor.getMaximumRange()) {
            if (mProximity == null || !mProximity) {
                mProximity = true;
                mServerConnection.updateState(mSensorItem, "CLOSED");
            }
        } else if (mProximity == null || mProximity) {
            mProximity = false;
            mServerConnection.updateState(mSensorItem, "OPEN");
        }
    }

    protected synchronized void addStatusItems(ApplicationStatus status) {
        if (mSensorEnabled) {
            String state = mCtx.getString(R.string.enabled);
            if (!mSensorItem.isEmpty()) {
                state += "\n" + mCtx.getString(R.string.objectClose, mProximity, mSensorItem, mSensorState);
            }

            state += "\n" + mCtx.getString(R.string.maxRangeResolution, mSensor.getMaximumRange(), mSensor.getResolution());

            status.set(mCtx.getString(R.string.pref_proximity), state);
        } else {
            status.set(mCtx.getString(R.string.pref_proximity), mCtx.getString(R.string.disabled));
        }
    }
} public class ScreenMonitor implements IDeviceMonitor, IStateUpdateListener {
    private static final String TAG = "HPV-ScreenMonitor";

    private final Context mCtx;
    private final ServerConnection mServerConnection;

    private final BroadcastReceiver mScreenReceiver;
    private boolean mMonitorEnabled;
    private boolean mReceiverRegistered;

    private String mScreenOnItem;
    private String mScreenOnState;
    private boolean mScreenOn;
    private final IntentFilter mIntentFilter;

    public ScreenMonitor(Context context, ServerConnection serverConnection) {
        mCtx = context;
        mServerConnection = serverConnection;

        EventBus.getDefault().register(this);

        mScreenReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                mScreenOn = Intent.ACTION_SCREEN_ON.equals(intent.getAction());
                if (mMonitorEnabled && mScreenOnItem != null) {
                    mServerConnection.updateState(mScreenOnItem, mScreenOn ? "CLOSED" : "OPEN");
                }
            }
        };

        mIntentFilter = new IntentFilter();
        mIntentFilter.addAction(Intent.ACTION_SCREEN_ON);
        mIntentFilter.addAction(Intent.ACTION_SCREEN_OFF);
    }

    @Override
    public void disablePreferences(Intent intent) { }

    @Override
    public synchronized void terminate() {
        EventBus.getDefault().unregister(this);
        try {
            mCtx.unregisterReceiver(mScreenReceiver);
            Log.d(TAG, "unregistering screen receiver...");
        } catch (IllegalArgumentException e) {
            // not registered
        }
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onMessageEvent(ApplicationStatus status) {
        if (mMonitorEnabled) {
            String state = mCtx.getString(R.string.enabled);
            if (!mScreenOnItem.isEmpty()) {
                state += "\n" + mCtx.getString(R.string.screenOn, mScreenOn, mScreenOnItem, mScreenOnState);
            }
            status.set(mCtx.getString(R.string.pref_screen), state);
        } else {
            status.set(mCtx.getString(R.string.pref_screen), mCtx.getString(R.string.disabled));
        }
    }

    @Override
    public void itemUpdated(String name, String value) {
        if (name.equals(mScreenOnItem)) {
            mScreenOnState = value;
        }
    }

    @Override
    public synchronized void updateFromPreferences(SharedPreferences prefs) {
        mScreenOnItem = prefs.getString(Constants.PREF_SCREEN_ITEM, "");

        if (mReceiverRegistered != (prefs.getBoolean(Constants.PREF_SCREEN_ENABLED, false))) {
            mReceiverRegistered = !mReceiverRegistered;

            mMonitorEnabled = prefs.getBoolean(Constants.PREF_SCREEN_ENABLED, false);

            if (mReceiverRegistered) {
                PowerManager powerManager = (PowerManager) mCtx.getSystemService(POWER_SERVICE);
                mScreenOn = powerManager.isScreenOn();

                if (mMonitorEnabled && mScreenOnItem != null) {
                    mServerConnection.updateState(mScreenOnItem, mScreenOn ? "CLOSED" : "OPEN");
                }

                Log.d(TAG, "registering screen receiver...");
                mCtx.registerReceiver(mScreenReceiver, mIntentFilter);
            } else {
                Log.d(TAG, "unregistering screen receiver...");
                mCtx.unregisterReceiver(mScreenReceiver);
            }
        } else if (mMonitorEnabled != prefs.getBoolean(Constants.PREF_SCREEN_ENABLED, false)) {
            mMonitorEnabled = !mMonitorEnabled;

            if (mMonitorEnabled && mScreenOnItem != null) {
                mServerConnection.updateState(mScreenOnItem, mScreenOn ? "CLOSED" : "OPEN");
            }
        }

        mServerConnection.subscribeItems(this, mScreenOnItem);
    }
} public class TemperatureMonitor extends AbstractAveragingDeviceMonitor {
    public TemperatureMonitor(Context ctx, SensorManager sensorManager, ServerConnection serverConnection) {
        super(ctx, sensorManager, serverConnection, ctx.getString(R.string.pref_temperature), "temperature", Sensor.TYPE_AMBIENT_TEMPERATURE);
    }


    @Override
    String getInfoString(Float value, String item, String state) {
        return mCtx.getString(R.string.temperature, mValue, mSensorItem, mSensorState);
    }
} public class VolumeMonitor implements IDeviceMonitor, IStateUpdateListener {
    private final Context mCtx;
    private final AudioManager mAudioManager;
    private final ServerConnection mServerConnection;

    private boolean mVolumeEnabled;

    private String mVolumeItem;
    private Integer mVolume;
    private final Integer mMaxVolume;
    private String mVolumeState;

    private final ContentObserver mVolumeObserver = new ContentObserver(new Handler()) {
        @Override
        public void onChange(boolean selfChange) {
            super.onChange(selfChange);

            mVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
            mServerConnection.updateState(mVolumeItem, String.valueOf(mVolume));
        }
    };

    public VolumeMonitor(Context context, AudioManager audioManager, ServerConnection serverConnection) {
        mCtx = context;
        mAudioManager = audioManager;
        mServerConnection = serverConnection;

        EventBus.getDefault().register(this);

        mMaxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
    }

    @Override
    public void disablePreferences(Intent intent) { }

    @Override
    public synchronized void updateFromPreferences(SharedPreferences prefs) {
        mVolumeItem = prefs.getString(Constants.PREF_VOLUME_ITEM, "");

        if (mVolumeEnabled != prefs.getBoolean(Constants.PREF_VOLUME_ENABLED, false)) {
            mVolumeEnabled = !mVolumeEnabled;

            if (mVolumeEnabled) {
                mCtx.getContentResolver().registerContentObserver(
                        android.provider.Settings.System.CONTENT_URI, true, mVolumeObserver);

                mVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
                mServerConnection.updateState(mVolumeItem, String.valueOf(mVolume));
            } else {
                mCtx.getContentResolver().unregisterContentObserver(mVolumeObserver);
            }
        }

        mServerConnection.subscribeItems(this, mVolumeItem);
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onMessageEvent(ApplicationStatus status) {
        if (mVolumeEnabled) {
            String state = mCtx.getString(R.string.enabled);
            if (!mVolumeItem.isEmpty()) {
                state += "\n" + mCtx.getString(R.string.volumeIsOf, mVolume, mMaxVolume, mVolumeItem, mVolumeState);
            }
            status.set(mCtx.getString(R.string.pref_volume), state);
        } else {
            status.set(mCtx.getString(R.string.pref_volume), mCtx.getString(R.string.disabled));
        }
    }

    @Override
    public void terminate() {
        EventBus.getDefault().unregister(this);
        mCtx.getContentResolver().unregisterContentObserver(mVolumeObserver);
    }

    @Override
    public void itemUpdated(String name, String value) {
        mVolumeState = value;
    }
} public class Camera {

    private static final String TAG = "HPV-Camera";

    private final Handler mUiHandler = new Handler(Looper.getMainLooper());
    private final Handler mWorkHandler;

    private final Activity mContext;
    private final TextureView mPreviewView;

    private ICamera mImplementation;
    private SurfaceTexture mSurface;

    private CameraVersion mVersion;
    private boolean mShowPreview;
    private boolean mCameraFallback;


    private final List<ICamera.ILumaListener> mListeners = new ArrayList<>();

    public Camera(Activity ctx, TextureView tv, SharedPreferences prefs) {
        mContext = ctx;
        mPreviewView = tv;

        HandlerThread mWorker = new HandlerThread("CameraWorker");
        mWorker.start();
        mWorkHandler = new Handler(mWorker.getLooper());

        EventBus.getDefault().register(this);

        mCameraFallback = prefs.getBoolean(Constants.PREF_CAMERA_FALLBACK, false);

        mVersion = getCameraVersion(prefs);
        mImplementation = createCamera(mVersion);
    }

    @Override
    protected void finalize() throws Throwable {
        stopPreview();

        super.finalize();
    }

    public void setDeviceRotation(int rotation) {
        mWorkHandler.post(() -> {
            Log.d(TAG, "setting orientation...");
            mImplementation.setDeviceRotation(rotation);
            Log.d(TAG, "setting orientation finished");
        });
    }

    public synchronized void updateFromPreferences(SharedPreferences prefs) {
        mWorkHandler.post(() -> doUpdateFromPreferences(prefs));
    }

    public void terminate(CountDownLatch l) {
        mWorkHandler.post(() -> {
            Log.d(TAG, "terminating...");
            doTerminate();
            l.countDown();
            Log.d(TAG, "terminating finished");
        });
    }

    public int getSensorOrientation() {
        // this is intentionally not done in the worker thread, as it is only called in
        // MainActivity.onCreate and in code already running in the wt.
        return mImplementation.getCameraOrientation();
    }

    public void takePicture(ICamera.IPictureListener h,
                            int takeDelay,
                            int compQuality) {
        mWorkHandler.post(() -> {
            Log.d(TAG, "taking picture...");
            try {
                doTakePicture(h, takeDelay, compQuality);
            } catch (CameraException e) {
                h.error(e.getLocalizedMessage());
            } finally {
                Log.d(TAG, "taking picture finished");
            }
        });
    }

    public boolean canBeUsed() {
        return mVersion != CameraVersion.NONE;
    }

    void addLumaListener(ICamera.ILumaListener l) {
        synchronized (mListeners) {
            mListeners.add(l);
            mImplementation.addLumaListener(l);
        }

        if (mListeners.size() > 0 && !isPreviewRunning() && !mShowPreview) {
            mWorkHandler.post(() -> {
                try {
                    startPreview(new ICamera.LoggingPreviewListener());
                } catch (CameraException e) {
                    Log.e(TAG, "Could not enable MotionDetector", e);
                }
            });
        }
    }

    void removeLumaListener(ICamera.ILumaListener l) {
        synchronized (mListeners) {
            mListeners.remove(l);
            mImplementation.removeLumaListener(l);
        }

        if (mListeners.isEmpty() && !mShowPreview) {
            mWorkHandler.post(this::stopPreview);
        }
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onMessageEvent(ApplicationStatus status) {
        if (isPreviewRunning()) {
            status.set(mContext.getString(R.string.pref_camera), mContext.getString(R.string.enabled) + "\n"
                    + mContext.getString(R.string.resolution, 640, 480) + "\n"
                    + (mVersion == CameraVersion.V1 ? mContext.getString(R.string.camApiPreLollipop) : mContext.getString(R.string.camApi2)));
        } else if (mVersion == CameraVersion.NONE) {
            status.set(mContext.getString(R.string.pref_camera), ((CameraImplNone) mImplementation).getMessage());
        } else {
            status.set(mContext.getString(R.string.pref_camera), mContext.getString(R.string.disabled));
        }
    }

    private synchronized void doUpdateFromPreferences(SharedPreferences prefs) {
        try {
            CameraVersion v = getCameraVersion(prefs);

            boolean camFallback = prefs.getBoolean(Constants.PREF_CAMERA_FALLBACK, false);
            // if camera api or fallback changed, close old camera, create new one
            if (v != mVersion || camFallback != mCameraFallback) {
                mCameraFallback = camFallback;

                if (isPreviewRunning()) {
                    Log.d(TAG, "stopping preview...");
                    mImplementation.stopPreview();
                    Log.d(TAG, "stopping preview finished.");
                }
                if (isCameraLocked()) {
                    Log.d(TAG, "unlocking camera...");
                    mImplementation.unlockCamera();
                    Log.d(TAG, "unlocking camera finished.");
                }

                mImplementation = createCamera(v);

                for (ICamera.ILumaListener l : mListeners) {
                    mImplementation.addLumaListener(l);
                }
            }

            mShowPreview = prefs.getBoolean(Constants.PREF_MOTION_DETECTION_PREVIEW, false)
                    && canBeUsed();

            // ensure camera preview state is correct
            // - preview state ON or lumalisteners -> preview running
            // - preview state OFF and no lumalisteners -> preview not running
            boolean shouldRun = mShowPreview || !mListeners.isEmpty();

            // we have zo resize the previewView before starting the preview
            mUiHandler.post(() -> {
                // make sure preview view has the correct size
                ViewGroup.LayoutParams params =
                        mPreviewView.getLayoutParams();

                if (mShowPreview) {
                    params.height = 480;
                    params.width = 640;
                } else {
                    // if we have no preview, we still need a visible
                    // TextureView in order to have a working motion detection.
                    // Resize it to 1x1pxs so it does not get in the way.
                    params.height = 1;
                    params.width = 1;
                }
                mPreviewView.setLayoutParams(params);
            });

            if (shouldRun && !isPreviewRunning()) {
                Log.d(TAG, "locking camera...");
                mImplementation.lockCamera();
                Log.d(TAG, "locking camera finished");

                registerSurfaceListener(new ICamera.LoggingPreviewListener());
            } else if (isPreviewRunning() && !shouldRun) {
                Log.d(TAG, "stopping preview ...");
                mImplementation.stopPreview();
                Log.d(TAG, "stopping preview finished.");

                if (isCameraLocked()) {
                    Log.d(TAG, "unlocking camera ...");
                    mImplementation.unlockCamera();
                    Log.d(TAG, "unlocking camera finished.");
                }
            }

        } catch (CameraException e) {
            Log.e(TAG, "failed to update preview state", e);
            UiUtil.showSnackBar(mPreviewView, e.getMessage());
        }
    }

    private void doTerminate() {
        EventBus.getDefault().unregister(this);

        if (isPreviewRunning()) {
            stopPreview();
        }

        mVersion = CameraVersion.NONE;
        mImplementation = new CameraImplNone(mContext.getString(R.string.camAlreadyClosed));
    }


    private void doTakePicture(ICamera.IPictureListener h,
                                         int takeDelay,
                                         int compQuality) throws CameraException {
        boolean wasPreviewRunning = isPreviewRunning();

        final CountDownLatch latch = new CountDownLatch(1);
        final ICamera.IPreviewListener pl = new ICamera.IPreviewListener() {
            final AtomicBoolean pictureTaken = new AtomicBoolean();

            @Override
            public void started() {
                if (!pictureTaken.getAndSet(true)) {

                    h.progress(mContext.getString(R.string.previewStarted));

                    if (!wasPreviewRunning) {
                        try {
                            Thread.sleep(takeDelay);
                        } catch (InterruptedException e) {
                            Log.e(TAG, "interrupted while waiting for take picture delay");
                        }
                    }

                    Log.d(TAG, "mImplementation.takePicture...");
                    mImplementation.takePicture(new ICamera.IPictureListener() {

                        @Override
                        public void picture(byte[] data) {
                            h.progress(mContext.getString(R.string.pictureTaken));
                            Bitmap bmp = BitmapFactory.decodeByteArray(data, 0, data.length);

                            int mDeviceRotation = mContext.getWindowManager().getDefaultDisplay().getRotation();
                            Matrix matrix = new Matrix();
                            int rotation = getSensorOrientation() + 90 * mDeviceRotation;
                            matrix.postRotate(rotation);

                            Bitmap rotated = Bitmap.createBitmap(bmp, 0, 0, bmp.getWidth(), bmp.getHeight(), matrix, true);

                            ByteArrayOutputStream stream = new ByteArrayOutputStream();
                            rotated.compress(Bitmap.CompressFormat.JPEG, compQuality, stream);
                            h.picture(stream.toByteArray());

                            if (!wasPreviewRunning) {
                                stopPreview();
                            } else if (mVersion == CameraVersion.V1) {
                                try {
                                    stopPreview();
                                    startPreview(new ICamera.LoggingPreviewListener());
                                } catch (CameraException e) {
                                    Log.e(TAG, "Error restarting preview", e);
                                    h.error(e.getMessage());
                                }
                            }

                            latch.countDown();
                        }

                        @Override
                        public void error(String message) {
                            Log.e(TAG, message);
                            h.error(message);

                            try {
                                stopPreview();

                                if (wasPreviewRunning) {
                                    startPreview(new ICamera.LoggingPreviewListener());
                                }
                            } catch (CameraException e) {
                                Log.e(TAG, "Error restarting preview", e);
                                h.error(e.getMessage());
                            }
                            latch.countDown();
                        }

                        @Override
                        public void progress(String message) {
                            Log.d(TAG, message);
                            h.progress(message);
                        }
                    });
                    Log.d(TAG, "mImplementation.takePicture finished");
                }
            }

            @Override
            public void error(String message) {
                latch.countDown();
                h.error(message);
                Log.e(TAG, message);
            }

            @Override
            public void exception(Exception e) {
                latch.countDown();
                h.error(e.getMessage());
                Log.e(TAG, "", e);
            }

            @Override
            public void progress(String message) {
                Log.d(TAG, message);
                h.progress(message);
            }
        };

        startPreview(pl);
        try {
            latch.await();
        } catch (InterruptedException e) {
            throw new CameraException(mContext.getString(R.string.camFailedToTakePic), e);
        }
        Log.d(TAG, "mImplementation.takePicture latch counted down");
    }

    private void startPreview(ICamera.IPreviewListener previewListener) throws CameraException {
        if (!isCameraLocked()) {
            previewListener.progress(mContext.getString(R.string.lockingCamera));
            Log.d(TAG, "locking camera...");
            mImplementation.lockCamera();
            Log.d(TAG, "locking camera finished");
        }

        if (!mImplementation.isPreviewRunning()) {
            if (mSurface == null) {
                registerSurfaceListener(previewListener);
            } else {
                previewListener.progress(mContext.getString(R.string.startingPreview));
                Log.d(TAG, "starting preview...");
                mImplementation.startPreview(mSurface, previewListener);
                Log.d(TAG, "starting preview finished");
            }
        } else {
            previewListener.started();
        }
    }

    private void stopPreview() {
        if (mImplementation.isPreviewRunning()) {
            try {
                Log.d(TAG, "stopping preview...");
                mImplementation.stopPreview();
                Log.d(TAG, "stopping preview finished");
            } catch (CameraException e) {
                Log.e(TAG, "stopping preview failed", e);
            }
        }

        if (mImplementation.isCameraLocked()) {
            Log.d(TAG, "unlocking camera...");
            mImplementation.unlockCamera();
            Log.d(TAG, "unlocking camera finished");
        }
    }

    private boolean isCameraLocked() {
        return mImplementation.isCameraLocked();
    }

    private boolean isPreviewRunning() {
        return mImplementation.isPreviewRunning();
    }

    private void registerSurfaceListener(ICamera.IPreviewListener previewListener) {
        if (mSurface == null && mPreviewView.getSurfaceTexture() != null) {
            mSurface = mPreviewView.getSurfaceTexture();
        }

        if (mSurface != null) {
            Log.d(TAG, "we already have a surface. adding listener...");

            mPreviewView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
                @Override
                public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i1) {
                }

                @Override
                public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i1) {
                }

                @Override
                public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
                    Log.d(TAG, "surface texture destroyed: " + surfaceTexture);
                    return mSurface == null;
                }

                @Override
                public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
                }
            });
            Log.d(TAG, "starting preview...");
            mImplementation.startPreview(mSurface, previewListener);
            Log.d(TAG, "starting preview finished");
            return;
        }

        previewListener.progress(mContext.getString(R.string.waitingSurface));
        mPreviewView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
            @Override
            public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i1) {
                if (mSurface == null) {
                    previewListener.progress(mContext.getString(R.string.surfaceObtained));
                    mSurface = surfaceTexture;
                    Log.d(TAG, "starting preview...");
                    mImplementation.startPreview(mSurface, previewListener);
                    Log.d(TAG, "starting preview finished");
                } else {
                    mPreviewView.setSurfaceTexture(mSurface);
                }
            }

            @Override
            public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i1) {
            }

            @Override
            public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
                Log.d(TAG, "surface texture destroyed: " + surfaceTexture);
                return mSurface == null;
            }

            @Override
            public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
            }
        });
    }

    private ICamera createCamera(CameraVersion version) {
        try {
            if (version == CameraVersion.PERMISSION_MISSING) {
                return new CameraImplNone(mContext.getString(R.string.permissionMissing, Manifest.permission.CAMERA));
            }

            if (version == CameraVersion.V2) {
                mVersion = CameraVersion.V2;
                return new CameraImplV2(mContext, mPreviewView, mCameraFallback);
            }

            mVersion = CameraVersion.V1;
            return new CameraImplV1(mContext, mPreviewView, mCameraFallback);
        } catch (CameraException e) {
            mVersion = CameraVersion.NONE;
            return new CameraImplNone(e.getMessage());
        }
    }

    private CameraVersion getCameraVersion(SharedPreferences prefs) {
        if (ActivityCompat.checkSelfPermission(mContext, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
            return CameraVersion.PERMISSION_MISSING;
        }

        boolean newApi = prefs.getBoolean(Constants.PREF_MOTION_DETECTION_NEW_API, true);
        if (newApi) {
            return CameraVersion.V2;
        }

        return CameraVersion.V1;
    }

    enum CameraVersion {
        PERMISSION_MISSING, NONE, V1, V2
    }
} public interface IMotionDetector {
    void terminate();

    void updateFromPreferences(SharedPreferences prefs);

    Camera getCamera();
} public class MotionDetector extends Thread implements IMotionDetector, ICamera.ILumaListener {
    private static final String TAG = "HPV-MotionDetector";

    private final AtomicBoolean mStopped = new AtomicBoolean(false);

    private final Activity mContext;
    private boolean mEnabled;
    private int mBoxes = 50;

    private int mSleepTime;
    private int mLeniency = 20;
    private final MotionReporter mMotionReporter;
    private int mDetectionCount = 0;
    private int mFrameCount = 0;

    private LumaData mPreviousState;
    private Comparer mComparer;
    private final Camera mCamera;
    private final AtomicReference<LumaData> mLumaData = new AtomicReference<>();

    public MotionDetector(Activity context, Camera camera, IMotionListener l, ServerConnection serverConnection) {
        mContext = context;
        mCamera = camera;

        mMotionReporter = new MotionReporter(l, serverConnection);
        EventBus.getDefault().register(this);

        setDaemon(true);
        start();
    }

    public void run() {
        try {
            while (!mStopped.get()) {
                try {
                    Thread.sleep(mSleepTime);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }

                LumaData greyState = getLumaData();

                if (greyState != null) {
                    Log.v(TAG, "processing frame");

                    mFrameCount++;

                    int minLuma = 1000;
                    if (greyState.isDarker(minLuma)) {
                        Log.v(TAG, "too dark");
                        mMotionReporter.tooDark();
                    } else {
                        ArrayList<Point> differing = detect(greyState);
                        if (differing != null && !differing.isEmpty()) {
                            mDetectionCount++;
                            mMotionReporter.motionDetected(differing);
                            Log.v(TAG, "motion");
                        } else {
                            mMotionReporter.noMotion();
                        }
                    }
                }
            }
        } finally {
            Log.v(TAG, "motion thread finished");
        }
    }

    private LumaData getLumaData() {
        return mLumaData.getAndSet(null);
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onMessageEvent(ApplicationStatus status) {
        if (mEnabled) {
            Resources res = mContext.getResources();
            status.set(mContext.getString(R.string.pref_motion), mContext.getString(R.string.enabled) + "\n" + (mContext.getString(R.string.resolution, 640, 480) + "\n"
                    + res.getQuantityString(R.plurals.boxesLeniency, mBoxes, mBoxes, mLeniency) + "\n"
                    + res.getQuantityString(R.plurals.frames, mFrameCount, mFrameCount)
                    + res.getQuantityString(R.plurals.motionDetected, mDetectionCount, mDetectionCount) + "\n"
                    + mContext.getString(R.string.timeBetweenDetections, mSleepTime)));
        } else {
            status.set(mContext.getString(R.string.pref_motion), mContext.getString(R.string.disabled));
        }
    }

    @Override
    public synchronized void terminate() {
        EventBus.getDefault().unregister(this);

        mMotionReporter.terminate();
        stopDetection();

        mStopped.set(true);
    }

    @Override
    public synchronized void updateFromPreferences(SharedPreferences prefs) {
        boolean newEnabled = prefs.getBoolean(Constants.PREF_MOTION_DETECTION_ENABLED, false);
        int newBoxes = Integer.parseInt(prefs.getString(Constants.PREF_MOTION_DETECTION_GRANULARITY, "20"));
        int newLeniency = Integer.parseInt(prefs.getString(Constants.PREF_MOTION_DETECTION_LENIENCY, "20"));

        mSleepTime = Integer.parseInt(prefs.getString(Constants.PREF_MOTION_DETECTION_SLEEP, "500"));

        if (newEnabled) {
            boolean changed = newBoxes != mBoxes || newLeniency != mLeniency;

            if (changed) {
                mComparer = null;
                mPreviousState = null;
            }

            if (!mEnabled) {
                mMotionReporter.updateFromPreferences(prefs);

                mBoxes = newBoxes;
                mLeniency = newLeniency;
                mDetectionCount = 0;
                mFrameCount = 0;

                mCamera.addLumaListener(this);
                mEnabled = true;
            }
        } else if (mEnabled) {
            stopDetection();
        }
    }

    @Override
    public Camera getCamera() {
        return mCamera;
    }

    private synchronized ArrayList<Point> detect(LumaData s) {
        if (mComparer == null) {
            mComparer = new Comparer(s.getWidth(), s.getHeight(), mBoxes, mLeniency);
        }

        if (mPreviousState == null) {
            mPreviousState = s;
            return null;
        }

        if (s.getWidth() != mPreviousState.getWidth() || s.getHeight() != mPreviousState.getHeight()) {
            return null;
        }

        ArrayList<Point> differing = mComparer.isDifferent(s, mPreviousState);
        mPreviousState.release();
        mPreviousState = s;

        return differing;
    }

    private synchronized void stopDetection() {
        mCamera.removeLumaListener(this);
        mEnabled = false;
        mComparer = null;
        mPreviousState = null;
    }

    @Override
    public void preview(LumaData greyState) {
        mLumaData.set(greyState);
    }

    @Override
    public boolean needsPreview() {
        return mLumaData.get() == null;
    }
} public class MotionVisualizer implements IMotionListener {
    private final SurfaceView mMotionView;
    private final NavigationView mNavigationView;
    private final SharedPreferences mPreferences;
    private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    private final int mMotionTextWidth;
    private final int mDarkTextWidth;

    private final int mCameraRotation;
    private int mCorrectionAngle;

    public MotionVisualizer(SurfaceView motionView, NavigationView navigationView, SharedPreferences preferences, int cameraRotation, int scaledSize) {
        mMotionView = motionView;
        mNavigationView = navigationView;
        mPreferences = preferences;
        mCameraRotation = cameraRotation;

        int newDeviceRotation = ((Activity) mMotionView.getContext()).getWindowManager().getDefaultDisplay().getRotation();
        setDeviceRotation(newDeviceRotation);

        mMotionView.setZOrderOnTop(true);
        mMotionView.getHolder().setFormat(PixelFormat.TRANSPARENT);

        mPaint.setColor(Color.WHITE);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setTextSize(scaledSize);

        Rect bounds = new Rect();
        mPaint.getTextBounds(mNavigationView.getContext().getString(R.string.motion), 0, 6, bounds);
        mMotionTextWidth = bounds.width();

        mPaint.getTextBounds(mNavigationView.getContext().getString(R.string.tooDark), 0, 8, bounds);
        mDarkTextWidth = bounds.width();
    }

    @Override
    public void motionDetected(ArrayList<Point> differing) {
        boolean showPreview = mPreferences.getBoolean(Constants.PREF_MOTION_DETECTION_PREVIEW, false);
        boolean motionDetection = mPreferences.getBoolean(Constants.PREF_MOTION_DETECTION_ENABLED, false);

        if (showPreview && motionDetection && mMotionView.getHolder().getSurface().isValid()) {
            final Canvas canvas = mMotionView.getHolder().lockCanvas();

            if (canvas != null) {
                try {
                    int boxes = Integer.parseInt(mPreferences.getString(Constants.PREF_MOTION_DETECTION_GRANULARITY, "20"));
                    float xsize = canvas.getWidth() / (float) boxes;
                    float ysize = canvas.getHeight() / (float) boxes;

                    canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);

                    if (mNavigationView.isShown()) {
                        Rect r1 = new Rect();
                        mNavigationView.getGlobalVisibleRect(r1);

                        Rect r2 = new Rect();
                        mMotionView.getGlobalVisibleRect(r2);

                        if (r1.left == 0) {
                            // drawer from left
                            canvas.clipRect(r1.right - r2.left, 0, r2.right, r2.bottom);
                        } else {
                            // drawer from right
                            canvas.clipRect(0, 0, r1.left - r2.left, r2.bottom);
                        }
                    }

                    canvas.drawText(mNavigationView.getContext().getString(R.string.motion), (canvas.getWidth() - mMotionTextWidth) / 2f, 50, mPaint);

                    for (Point p : differing) {
                        Point c = correctSensorRotation(p, mCorrectionAngle, boxes);
                        canvas.drawRect(c.x * xsize, c.y * ysize, c.x * xsize + xsize, c.y * ysize + ysize, mPaint);
                    }
                } finally {
                    mMotionView.getHolder().unlockCanvasAndPost(canvas);
                }
            }
        }
    }

    private Point correctSensorRotation(Point p, int correctionAngle, int boxes) {
        if (correctionAngle == 270) {
            return new Point(boxes - 1 - p.y, boxes - 1 - p.x);
        } else if (correctionAngle == 180) {
            return new Point(boxes - 1 - p.x, p.y);
        } else if (correctionAngle == 90) {
            return new Point(p.y, boxes - 1 - p.x);
        } else {
            return new Point(p.x, boxes - 1 - p.y);
        }
    }

    @Override
    public void noMotion() {
        boolean showPreview = mPreferences.getBoolean(Constants.PREF_MOTION_DETECTION_PREVIEW, false);
        boolean motionDetection = mPreferences.getBoolean(Constants.PREF_MOTION_DETECTION_ENABLED, false);

        if (showPreview && motionDetection && mMotionView.getHolder().getSurface().isValid()) {
            final Canvas canvas = mMotionView.getHolder().lockCanvas();
            if (canvas != null) {
                canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
                mMotionView.getHolder().unlockCanvasAndPost(canvas);
            }
        }
    }

    @Override
    public void tooDark() {
        boolean showPreview = mPreferences.getBoolean(Constants.PREF_MOTION_DETECTION_PREVIEW, false);
        boolean motionDetection = mPreferences.getBoolean(Constants.PREF_MOTION_DETECTION_ENABLED, false);

        if (showPreview && motionDetection && mMotionView.getHolder().getSurface().isValid()) {
            final Canvas canvas = mMotionView.getHolder().lockCanvas();
            if (canvas != null) {
                canvas.drawText(mNavigationView.getContext().getString(R.string.tooDark), (canvas.getWidth() - mDarkTextWidth) / 2f, 50, mPaint);
                mMotionView.getHolder().unlockCanvasAndPost(canvas);
            }
        }
    }

    public void setDeviceRotation(int newDeviceRotation) {
        mCorrectionAngle = (mCameraRotation - newDeviceRotation * 90 + 360) % 360;
    }
} public class ApplicationStatus {
    private final ArrayList<StatusItem> mValues = new ArrayList<>();
    private final HashMap<String, StatusItem> mIndices = new HashMap<>();

    public synchronized void set(String key, String value) {
        StatusItem item = mIndices.get(key);
        if (item == null) {
            item = new StatusItem(key, value);
            mValues.add(item);
            mIndices.put(key, item);
        } else {
            item.setValue(value);
        }
    }

    int getItemCount() {
        return mValues.size();
    }

    StatusItem getItem(int i) {
        return mValues.get(i);
    }
} public class StatusInfoActivity extends ScreenControllingActivity {
    private final ApplicationStatus status = new ApplicationStatus();
    private ScheduledExecutorService executor;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        final StatusItemAdapter adapter = new StatusItemAdapter(this, status);

        executor = Executors.newSingleThreadScheduledExecutor();
        executor.scheduleWithFixedDelay(() -> {
            EventBus.getDefault().post(status);

            runOnUiThread(adapter::notifyDataSetChanged);
        }, 0, 1, TimeUnit.SECONDS);

        setContentView(R.layout.activity_statusinfo);

        Toolbar myToolbar = findViewById(R.id.toolbar);
        setSupportActionBar(myToolbar);

        ActionBar ab = getSupportActionBar();
        if (ab != null) {
            ab.setDisplayHomeAsUpEnabled(true);
        }

        final ListView listview = findViewById(R.id.info_listview);
        listview.setAdapter(adapter);
    }

    @Override
    public View getScreenOnView() {
        return findViewById(R.id.info_listview);
    }

    @Override
    protected void onDestroy() {
        if (executor != null) {
            executor.shutdown();
            executor = null;
        }
        super.onDestroy();
    }

    private static class StatusItemAdapter extends BaseAdapter {
        private final Activity mContext;
        private final ApplicationStatus mStatus;

        StatusItemAdapter(Activity context, ApplicationStatus status) {
            mContext = context;
            mStatus = status;
        }

        @Override
        public int getCount() {
            return mStatus.getItemCount();
        }

        @Override
        public StatusItem getItem(int i) {
            return mStatus.getItem(i);
        }

        @Override
        public long getItemId(int i) {
            return mStatus.getItem(i).getId();
        }

        @NonNull
        @Override
        public View getView(int position, View view, @NonNull ViewGroup parent) {
            LayoutInflater inflater = mContext.getLayoutInflater();
            View rowView= inflater.inflate(R.layout.row_commandlog, parent, false);

            TextView txtTitle = rowView.findViewById(R.id.name);
            TextView txtValue = rowView.findViewById(R.id.value);
            txtTitle.setText(getItem(position).getName());
            txtValue.setText(getItem(position).getValue());

            return rowView;
        }

        @Override
        public CharSequence[] getAutofillOptions() {
            return new CharSequence[0];
        }
    }
}
import android.app.Activity;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.bluetooth.BluetoothManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.Point;
import android.hardware.SensorManager;
import android.hardware.camera2.CameraManager;
import android.media.AudioManager;
import android.media.projection.MediaProjection;
import android.media.projection.MediaProjectionManager;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.PowerManager;
import android.preference.PreferenceManager;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.ContextMenu;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.SurfaceView;
import android.view.TextureView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.app.NotificationCompat;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import com.google.android.material.navigation.NavigationView;
import com.jakewharton.processphoenix.ProcessPhoenix;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Date;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import de.vier_bier.habpanelviewer.command.AdminHandler;
import de.vier_bier.habpanelviewer.command.BluetoothHandler;
import de.vier_bier.habpanelviewer.command.CommandQueue;
import de.vier_bier.habpanelviewer.command.FlashHandler;
import de.vier_bier.habpanelviewer.command.InternalCommandHandler;
import de.vier_bier.habpanelviewer.command.NotificationHandler;
import de.vier_bier.habpanelviewer.command.ScreenHandler;
import de.vier_bier.habpanelviewer.command.TtsHandler;
import de.vier_bier.habpanelviewer.command.VolumeHandler;
import de.vier_bier.habpanelviewer.command.WebViewHandler;
import de.vier_bier.habpanelviewer.command.log.CommandLogActivity;
import de.vier_bier.habpanelviewer.connection.ConnectionStatistics;
import de.vier_bier.habpanelviewer.connection.OkHttpClientFactory;
import de.vier_bier.habpanelviewer.connection.ssl.CertificateManager;
import de.vier_bier.habpanelviewer.db.CredentialManager;
import de.vier_bier.habpanelviewer.help.HelpActivity;
import de.vier_bier.habpanelviewer.openhab.ISseConnectionListener;
import de.vier_bier.habpanelviewer.openhab.ServerConnection;
import de.vier_bier.habpanelviewer.openhab.SseConnection;
import de.vier_bier.habpanelviewer.preferences.PreferenceActivity;
import de.vier_bier.habpanelviewer.reporting.AccelerometerMonitor;
import de.vier_bier.habpanelviewer.reporting.BatteryMonitor;
import de.vier_bier.habpanelviewer.reporting.BrightnessMonitor;
import de.vier_bier.habpanelviewer.reporting.ConnectedIndicator;
import de.vier_bier.habpanelviewer.reporting.DockingStateMonitor;
import de.vier_bier.habpanelviewer.reporting.IDeviceMonitor;
import de.vier_bier.habpanelviewer.reporting.NoiseLevelMonitor;
import de.vier_bier.habpanelviewer.reporting.PressureMonitor;
import de.vier_bier.habpanelviewer.reporting.ProximityMonitor;
import de.vier_bier.habpanelviewer.reporting.ScreenMonitor;
import de.vier_bier.habpanelviewer.reporting.TemperatureMonitor;
import de.vier_bier.habpanelviewer.reporting.VolumeMonitor;
import de.vier_bier.habpanelviewer.reporting.motion.Camera;
import de.vier_bier.habpanelviewer.reporting.motion.IMotionDetector;
import de.vier_bier.habpanelviewer.reporting.motion.MotionDetector;
import de.vier_bier.habpanelviewer.reporting.motion.MotionVisualizer;
import de.vier_bier.habpanelviewer.status.ApplicationStatus;
import de.vier_bier.habpanelviewer.status.StatusInfoActivity;
package de.vier_bier.habpanelviewer;







/**
 * Main activity showing the Webview for openHAB.
 */
public class MainActivity extends ScreenControllingActivity
        implements NavigationView.OnNavigationItemSelectedListener {
    private static final String TAG = "HPV-MainActivity";

    private ClientWebView mWebView;
    private TextView mUrlTextView;
    private TextView mStatusTextView;

    private ISseConnectionListener mConnectionListener;
    private ConnectionStatistics mConnections;
    private ServerConnection mServerConnection;
    private AppRestartingExceptionHandler mRestartingExceptionHandler;
    private NetworkTracker mNetworkTracker;
    private FlashHandler mFlashService;
    private IMotionDetector mMotionDetector;