content: /*
   * Copyright (C) 2014 AChep@xda <artemchep@gmail.com>
   *
   * This program is free software; you can redistribute it and/or
   * modify it under the terms of the GNU General Public License
   * as published by the Free Software Foundation; either version 2
   * of the License, or (at your option) any later version.
   *
   * This program is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   * GNU General Public License for more details.
   *
   * You should have received a copy of the GNU General Public License
   * along with this program; if not, write to the Free Software
   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
   * MA  02110-1301, USA.
   */
  package com.achep.acdisplay.services.activemode;
  
  import android.content.BroadcastReceiver;
  import android.content.Context;
  import android.content.Intent;
  import android.content.IntentFilter;
  import android.hardware.Sensor;
  import android.hardware.SensorManager;
  import android.os.PowerManager;
  import android.os.SystemClock;
  import androidx.annotation.NonNull;
  import androidx.localbroadcastmanager.content.LocalBroadcastManager;
  import android.util.Log;
  
  import com.achep.acdisplay.App;
  import com.achep.acdisplay.Atomic;
  import com.achep.acdisplay.Config;
  import com.achep.acdisplay.Presenter;
  import com.achep.acdisplay.R;
  import com.achep.acdisplay.notifications.NotificationPresenter;
  import com.achep.acdisplay.notifications.OpenNotification;
  import com.achep.acdisplay.services.BathService;
  import com.achep.acdisplay.services.Switch;
  import com.achep.acdisplay.services.SwitchService;
  import com.achep.acdisplay.services.activemode.sensors.AccelerometerSensor;
  import com.achep.acdisplay.services.activemode.sensors.GyroscopeSensor;
  import com.achep.acdisplay.services.activemode.sensors.ProximitySensor;
  import com.achep.acdisplay.services.switches.BatteryOutSwitch;
  import com.achep.acdisplay.services.switches.InactiveTimeSwitch;
  import com.achep.acdisplay.services.switches.NoNotifiesSwitch;
  import com.achep.acdisplay.services.switches.ScreenOffSwitch;
  import com.achep.base.AppHeap;
  import com.achep.base.content.ConfigBase;
  import com.achep.base.tests.Check;
  import com.achep.base.utils.power.PowerUtils;
  
  import static com.achep.base.Build.DEBUG;
  
  /**
   * Service that turns on AcDisplay exactly when it's needed.
   *
   * @author Artem Chepurnoy
   * @see com.achep.acdisplay.services.activemode.ActiveModeSensor
   */
  public class ActiveModeService extends SwitchService implements
          NotificationPresenter.OnNotificationListChangedListener, ActiveModeSensor.Callback {
  
      private static final String TAG = "ActiveModeService";
      private static final String WAKE_LOCK_TAG = "Consuming sensors";
  
      private ActiveModeSensor[] mSensors;
  
      private long mConsumingPingTimestamp;
      private PowerManager.WakeLock mWakeLock;
  
      private final BroadcastReceiver mLocalReceiver = new BroadcastReceiver() {
  
          @Override
          public void onReceive(Context context, Intent intent) {
              switch (intent.getAction()) {
                  case App.ACTION_INTERNAL_PING_SENSORS:
                      pingConsumingSensors();
                      break;
              }
          }
      };
  
      private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
  
          @Override
          public void onReceive(Context context, Intent intent) {
              switch (intent.getAction()) {
                  case Intent.ACTION_BATTERY_CHANGED:
                      mPluggedAtomic.react(PowerUtils.isPlugged(intent));
                      break;
              }
          }
      };
  
      private final Atomic.Callback mPluggedAtomicCallback = new Atomic.Callback() {
          @Override
          public void onStart(Object... objects) {
              if (DEBUG) Log.d(TAG, "Plugged: Start the consuming sensors");
              for (ActiveModeSensor ams : mSensors) {
                  Check.getInstance().isTrue(ams.isAttached());
                  if (ams instanceof ActiveModeSensor.Consuming) {
                      ActiveModeSensor.Consuming sensor = (ActiveModeSensor.Consuming) ams;
                      sensor.start();
                  }
              }
          }
  
          @Override
          public void onStop(Object... objects) {
              if (DEBUG) Log.d(TAG, "Unplugged: Stop the consuming sensors");
              for (ActiveModeSensor ams : mSensors) {
                  Check.getInstance().isTrue(ams.isAttached());
                  if (ams instanceof ActiveModeSensor.Consuming) {
                      ActiveModeSensor.Consuming sensor = (ActiveModeSensor.Consuming) ams;
                      sensor.stop();
                  }
              }
          }
      };
  
      private final Atomic mPluggedAtomic = new Atomic(mPluggedAtomicCallback, TAG + ":Plugged");
      private boolean mActiveChargingEnabled;
  
      /**
       * Starts or stops this service as required by settings and device's state.
       */
      public static void handleState(@NonNull Context context) {
          Config config = Config.getInstance();
  
          boolean onlyWhileChangingOption = !config.isEnabledOnlyWhileCharging()
                  || PowerUtils.isPlugged(context);
  
          if (config.isEnabled()
                  && config.isActiveModeEnabled()
                  && onlyWhileChangingOption) {
              BathService.startService(context, ActiveModeService.class);
          } else {
              BathService.stopService(context, ActiveModeService.class);
          }
      }
  
      public static boolean isSupported(@NonNull Context context) {
          SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
          return sensorManager.getSensorList(Sensor.TYPE_PROXIMITY).size() > 0;
      }
  
      /**
       * Builds the array of supported {@link ActiveModeSensor sensors}.
       *
       * @return The array of supported {@link ActiveModeSensor sensors}.
       * @see ActiveModeSensor
       */
      @NonNull
      public static ActiveModeSensor[] buildAvailableSensorsList(@NonNull Context context) {
          SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
          ActiveModeSensor[] sensors = new ActiveModeSensor[]{ // all available sensors
                  AccelerometerSensor.getInstance(),
                  GyroscopeSensor.getInstance(),
                  ProximitySensor.getInstance()
          };
  
          // Count the number of supported sensors, and
          // mark unsupported.
          int count = sensors.length;
          boolean[] supportList = new boolean[sensors.length];
          for (int i = 0; i < sensors.length; i++) {
              supportList[i] = sensors[i].isSupported(sensorManager);
              if (!supportList[i]) {
                  count--;
              }
          }
  
          // Create the list of proven sensors.
          ActiveModeSensor[] sensorsSupported = new ActiveModeSensor[count];
          for (int i = 0, j = 0; i < sensors.length; i++) {
              if (supportList[i]) {
                  sensorsSupported[j++] = sensors[i];
              }
          }
  
          return sensorsSupported;
      }
  
      @NonNull
      @Override
      public Switch[] onBuildSwitches() {
          Config config = Config.getInstance();
          ConfigBase.Option noNotifies = config.getOption(Config.KEY_ACTIVE_MODE_WITHOUT_NOTIFICATIONS);
          ConfigBase.Option respectIt = config.getOption(Config.KEY_ACTIVE_MODE_RESPECT_INACTIVE_TIME);
          ConfigBase.Option batteryOut = config.getOption(Config.KEY_ACTIVE_MODE_DISABLE_ON_LOW_BATTERY);
          return new Switch[]{
                  new ScreenOffSwitch(getContext(), this),
                  new InactiveTimeSwitch(getContext(), this, respectIt),
                  new NoNotifiesSwitch(getContext(), this, noNotifies, true),
                  new BatteryOutSwitch(getContext(), this, batteryOut, false),
          };
      }
  
      @Override
      public void onCreate() {
          Context context = getContext();
          mSensors = buildAvailableSensorsList(context);
          super.onCreate();
  
          IntentFilter filter = new IntentFilter();
          filter.addAction(App.ACTION_INTERNAL_PING_SENSORS);
          LocalBroadcastManager.getInstance(context).registerReceiver(mLocalReceiver, filter);
  
          NotificationPresenter.getInstance().registerListener(this);
      }
  
      @Override
      public void onDestroy() {
          LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(mLocalReceiver);
          NotificationPresenter.getInstance().unregisterListener(this);
          super.onDestroy();
  
          // Watch for the leaks
          AppHeap.getRefWatcher().watch(this);
      }
  
      @Override
      public String getLabel() {
          return getContext().getString(R.string.service_bath_active_mode);
      }
  
      @Override
      public void onNotificationListChanged(@NonNull NotificationPresenter np,
                                            OpenNotification osbn,
                                            int event, boolean isLastEventInSequence) {
          if (Config.getInstance().isNotifyWakingUp()) {
              // Notification will wake up device without
              // any sensors' callback.
              return;
          }
  
          switch (event) {
              case NotificationPresenter.EVENT_CHANGED:
              case NotificationPresenter.EVENT_POSTED:
                  pingConsumingSensors();
                  break;
          }
      }
  
      @Override
      public void onStart(Object... objects) {
          if (DEBUG) Log.d(TAG, "Starting listening to sensors.");
  
          Context context = getContext();
          SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
          for (ActiveModeSensor sensor : mSensors) {
              sensor.registerCallback(this);
              sensor.onAttached(sensorManager, context);
          }
  
          mActiveChargingEnabled = Config.getInstance().isActiveModeActiveChargingEnabled();
          if (mActiveChargingEnabled) {
              mPluggedAtomic.react(PowerUtils.isPlugged(context));
  
              IntentFilter intentFilter = new IntentFilter();
              intentFilter.addAction(Intent.ACTION_BATTERY_CHANGED);
              context.registerReceiver(mReceiver, intentFilter);
          }
      }
  
      @Override
      public void onStop(Object... objects) {
          if (DEBUG) Log.d(TAG, "Stopping listening to sensors.");
  
          if (mActiveChargingEnabled) {
              Context context = getContext();
              context.unregisterReceiver(mReceiver);
              mPluggedAtomic.stop();
          }
  
          for (ActiveModeSensor sensor : mSensors) {
              sensor.onDetached();
              sensor.unregisterCallback(this);
          }
  
          releaseWakeLock();
      }
  
      /**
       * {@inheritDoc}
       */
      public void pingConsumingSensors() {
          mConsumingPingTimestamp = SystemClock.elapsedRealtime();
          pingConsumingSensorsInternal();
      }
  
      private void pingConsumingSensorsInternal() {
          // Find maximum remaining time.
          int remainingTime = -1;
          for (ActiveModeSensor ams : mSensors) {
              if (ams.isAttached() && ams instanceof ActiveModeSensor.Consuming) {
                  ActiveModeSensor.Consuming sensor = (ActiveModeSensor.Consuming) ams;
                  remainingTime = Math.max(remainingTime, sensor.getRemainingTime());
              }
          }
  
          long now = SystemClock.elapsedRealtime();
          int delta = (int) (now - mConsumingPingTimestamp);
  
          remainingTime -= delta;
          if (remainingTime < 0) {
              return; // Too late
          }
  
          // Acquire wake lock to be sure that sensors will be fine.
          releaseWakeLock();
          PowerManager pm = (PowerManager) getContext().getSystemService(Context.POWER_SERVICE);
          mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG);
          mWakeLock.acquire(remainingTime);
  
          // Ping sensors
          for (ActiveModeSensor ams : mSensors) {
              if (ams.isAttached() && ams instanceof ActiveModeSensor.Consuming) {
                  ActiveModeSensor.Consuming sensor = (ActiveModeSensor.Consuming) ams;
  
                  int sensorRemainingTime = sensor.getRemainingTime() - delta;
                  if (sensorRemainingTime > 0) {
                      sensor.ping(sensorRemainingTime);
                  }
              }
          }
      }
  
      private void releaseWakeLock() {
          if (mWakeLock != null && mWakeLock.isHeld()) {
              mWakeLock.release();
              mWakeLock = null;
          }
      }
  
      @Override
      public void onWakeRequested(@NonNull ActiveModeSensor sensor) {
          Presenter.getInstance().tryStartGuiCauseSensor(getContext());
      }
  
  }
  