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;
  
  import android.app.Notification;
  import android.app.NotificationManager;
  import android.content.BroadcastReceiver;
  import android.content.Context;
  import android.content.Intent;
  import android.content.IntentFilter;
  import android.os.IBinder;
  import androidx.annotation.NonNull;
  import androidx.annotation.Nullable;
  import androidx.localbroadcastmanager.content.LocalBroadcastManager;
  import android.text.TextUtils;
  import android.util.Log;
  
  import com.achep.acdisplay.App;
  import com.achep.acdisplay.R;
  import com.achep.acdisplay.notifications.NotificationHelper;
  import com.achep.acdisplay.ui.activities.MainActivity;
  import com.achep.base.AppHeap;
  import com.achep.base.interfaces.IOnLowMemory;
  import com.achep.base.services.BaseService;
  
  import java.util.HashMap;
  import java.util.Map;
  import java.util.Set;
  import java.util.concurrent.ConcurrentHashMap;
  
  import static com.achep.base.Build.DEBUG;
  
  /**
   * Created by achep on 26.05.14.
   *
   * @author Artem Chepurnoy
   */
  public class BathService extends BaseService {
  
      private static final String TAG = "BathService";
  
      private static final String ACTION_ADD_SERVICE = TAG + ":add_service";
      private static final String ACTION_REMOVE_SERVICE = TAG + ":remove_service";
      private static final String EXTRA_SERVICE_CLASS = "class";
  
      public static void startService(Context context, Class<? extends ChildService> clazz) {
          synchronized (monitor) {
              if (sRunning) {
                  Intent intent = new Intent(ACTION_ADD_SERVICE);
                  intent.putExtra(EXTRA_SERVICE_CLASS, clazz);
                  LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
              } else if (!sServiceMap.containsKey(clazz)) {
                  ChildService instance;
                  try {
                      instance = clazz.newInstance();
                  } catch (Exception e) {
                      throw new RuntimeException(e);
                  }
  
                  sServiceMap.put(clazz, instance);
                  if (!sCreated) context.startService(new Intent(context, BathService.class));
              }
          }
      }
  
      public static void stopService(Context context, Class<? extends ChildService> clazz) {
          synchronized (monitor) {
              if (sRunning) {
                  Intent intent = new Intent(ACTION_REMOVE_SERVICE);
                  intent.putExtra(EXTRA_SERVICE_CLASS, clazz);
                  LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
              } else {
                  sServiceMap.remove(clazz);
              }
          }
      }
  
      private static final Map<Class, ChildService> sServiceMap = new ConcurrentHashMap<>(2);
      private static final Object monitor = new Object();
      private static boolean sCreated;
      private static boolean sRunning;
  
      private LocalBroadcastManager mLocalBroadcastManager;
      private NotificationManager mNotificationManager;
      private String mLanguage;
  
      private final Map<Class, ChildService> mMap = new HashMap<>(2);
      private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
          @Override
          public void onReceive(Context context, Intent intent) {
              final String action = intent.getAction();
              switch (action) {
                  // Received from a local broadcast receiver.
                  case ACTION_ADD_SERVICE:
                  case ACTION_REMOVE_SERVICE:
                      synchronized (monitor) {
                          Class clazz = (Class) intent.getSerializableExtra(EXTRA_SERVICE_CLASS);
                          boolean addition = ACTION_ADD_SERVICE.equals(action);
                          boolean exists = mMap.containsKey(clazz);
                          if (addition == exists) return;
                          if (addition) { // Addition
                              ChildService child;
                              try {
                                  // Adding child to host service.
                                  child = (ChildService) clazz.newInstance();
                              } catch (Exception e) {
                                  throw new RuntimeException(e); // Should never happen
                              }
                              child.setContext(BathService.this);
                              child.onCreate();
                              mMap.put(clazz, child);
  
                              updateNotification();
                          } else { // Removal
                              ChildService child = mMap.remove(clazz);
                              child.onDestroy();
                              child.setContext(null);
  
                              if (mMap.isEmpty()) {
                                  stopMySelf();
                              } else updateNotification();
                          }
                      }
                      break;
                  // Received from a system broadcast receiver.
                  case Intent.ACTION_CONFIGURATION_CHANGED:
                      String lang = getResources().getConfiguration().locale.getLanguage();
                      if (!TextUtils.equals(mLanguage, lang)) {
                          mLanguage = lang;
                          updateNotification();
                      }
                      break;
              }
          }
      };
  
      @Override
      public void onCreate() {
          super.onCreate();
          mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
          mLanguage = getResources().getConfiguration().locale.getLanguage();
  
          // Listen for the config changes to update notification just
          // once locale has changed.
          IntentFilter intentFilter = new IntentFilter();
          intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
          registerReceiver(mReceiver, intentFilter);
  
          synchronized (monitor) {
              sCreated = true;
              sRunning = true;
  
              // Register for add / remove service events.
              intentFilter = new IntentFilter();
              intentFilter.addAction(ACTION_ADD_SERVICE);
              intentFilter.addAction(ACTION_REMOVE_SERVICE);
              mLocalBroadcastManager = LocalBroadcastManager.getInstance(this);
              mLocalBroadcastManager.registerReceiver(mReceiver, intentFilter);
  
              if (sServiceMap.isEmpty()) {
                  stopMySelf();
              } else {
                  // Init all children
                  Set<Map.Entry<Class, ChildService>> set = sServiceMap.entrySet();
                  for (Map.Entry<Class, ChildService> entry : set) {
                      ChildService child = entry.getValue();
                      child.setContext(this);
                      child.onCreate();
  
                      mMap.put(entry.getKey(), child);
                  }
                  sServiceMap.clear();
  
                  startForeground(App.ID_NOTIFY_BATH, buildNotification());
              }
          }
      }
  
      @Override
      public void onLowMemory() {
          super.onLowMemory();
          synchronized (monitor) {
              for (ChildService child : mMap.values()) {
                  child.onLowMemory();
              }
          }
      }
  
      @Override
      public void onDestroy() {
          super.onDestroy();
          synchronized (monitor) {
              sCreated = false;
              sRunning = false;
              mLocalBroadcastManager.unregisterReceiver(mReceiver);
  
              // Kill all children.
              for (ChildService child : mMap.values()) child.onDestroy();
              mMap.clear();
  
              // TODO: Should I add children back to pending map and then
              // restart the service?
              if (!sServiceMap.isEmpty()) startService(new Intent(this, getClass()));
          }
  
          unregisterReceiver(mReceiver);
          // Make sure that notification does not exists.
          mNotificationManager.cancel(App.ID_NOTIFY_BATH);
  
          // Leaks canary
          AppHeap.getRefWatcher().watch(this);
      }
  
      private void stopMySelf() {
          sRunning = false;
          stopSelf();
      }
  
      private void updateNotification() {
          mNotificationManager.notify(App.ID_NOTIFY_BATH, buildNotification());
      }
  
      /**
       * Builds fresh notification with all {@link ChildService children services}'s
       * {@link com.achep.acdisplay.services.BathService.ChildService#getLabel() labels} in.
       */
      @NonNull
      private Notification buildNotification() {
          boolean empty = true;
          StringBuilder sb = new StringBuilder();
          String divider = getString(R.string.settings_multi_list_divider);
          for (ChildService child : mMap.values()) {
              String label = child.getLabel();
              if (TextUtils.isEmpty(label)) {
                  if (DEBUG) {
                      label = "[" + child.getClass().getSimpleName() + "]";
                  } else continue;
              }
              if (!empty) {
                  sb.append(divider);
              }
              sb.append(label);
              empty = false;
          }
  
          // Format a message text.
          String contentText = sb.toString();
          if (contentText.length() > 0 && !mLanguage.contains("de")) {
              contentText = contentText.charAt(0) + contentText.substring(1).toLowerCase();
          }
  
          // Get notification intent.
          Intent intent = null;
          for (ChildService child : mMap.values())
              if (!TextUtils.isEmpty(child.getLabel())) {
                  if (intent == null) {
                      intent = child.getSettingsIntent();
                  } else {
                      intent = null;
                      break;
                  }
              }
          if (intent == null) {
              intent = new Intent(this, MainActivity.class);
          }
  
          return NotificationHelper.buildNotification(this, App.ID_NOTIFY_BATH, contentText, intent);
      }
  
      @Override
      public IBinder onBind(Intent intent) {
          return null;
      }
  
      //-- CHILD SERVICE --------------------------------------------------------
  
      /**
       * Base for fake foreground service hosted in {@link com.achep.acdisplay.services.BathService}.
       * Call {@link BathService#startService(android.content.Context, Class)} to start this service,
       * and {@link BathService#stopService(android.content.Context, Class)} to stop.
       *
       * @author Artem Chepurnoy
       */
      public abstract static class ChildService implements IOnLowMemory {
  
          private Context mContext;
  
          public ChildService() {
              if (DEBUG) {
                  Log.d(TAG, "Creating " + getClass().getSimpleName() + " service...");
              }
          }
  
          final void setContext(Context context) {
              mContext = context;
          }
  
          /**
           * Called when fake-service is attached to main one.
           *
           * @see android.app.Service#onCreate()
           */
          public abstract void onCreate();
  
          /**
           * Called when fake-service is detached from main one.
           *
           * @see android.app.Service#onDestroy()
           */
          public abstract void onDestroy();
  
          /**
           * {@inheritDoc}
           */
          @Override
          public void onLowMemory() { /* placeholder */ }
  
          /**
           * @return The human-readable label of this service.
           */
          @Nullable
          public String getLabel() {
              return null;
          }
  
          @Nullable
          public Intent getSettingsIntent() {
              return null;
          }
  
          public final Context getContext() {
              return mContext;
          }
  
      }
  
  }
  