content: /*
   * Copyright (C) 2015 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.server.notification;
  
  import android.app.ActivityManager;
  import android.app.AlarmManager;
  import android.app.PendingIntent;
  import android.content.BroadcastReceiver;
  import android.content.ComponentName;
  import android.content.Context;
  import android.content.Intent;
  import android.content.IntentFilter;
  import android.net.Uri;
  import android.os.Binder;
  import android.provider.Settings;
  import android.service.notification.Condition;
  import android.service.notification.IConditionProvider;
  import android.service.notification.ScheduleCalendar;
  import android.service.notification.ZenModeConfig;
  import android.text.TextUtils;
  import android.util.ArrayMap;
  import android.util.ArraySet;
  import android.util.Log;
  import android.util.Slog;
  
  import com.android.internal.annotations.GuardedBy;
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.server.notification.NotificationManagerService.DumpFilter;
  
  import java.io.PrintWriter;
  import java.util.ArrayList;
  import java.util.Calendar;
  import java.util.List;
  
  /**
   * Built-in zen condition provider for daily scheduled time-based conditions.
   */
  public class ScheduleConditionProvider extends SystemConditionProviderService {
      static final String TAG = "ConditionProviders.SCP";
      static final boolean DEBUG = true || Log.isLoggable("ConditionProviders", Log.DEBUG);
  
      public static final ComponentName COMPONENT =
              new ComponentName("android", ScheduleConditionProvider.class.getName());
      private static final String NOT_SHOWN = "...";
      private static final String SIMPLE_NAME = ScheduleConditionProvider.class.getSimpleName();
      private static final String ACTION_EVALUATE =  SIMPLE_NAME + ".EVALUATE";
      private static final int REQUEST_CODE_EVALUATE = 1;
      private static final String EXTRA_TIME = "time";
      private static final String SEPARATOR = ";";
      private static final String SCP_SETTING = "snoozed_schedule_condition_provider";
  
      private final Context mContext = this;
      private final ArrayMap<Uri, ScheduleCalendar> mSubscriptions = new ArrayMap<>();
      private ArraySet<Uri> mSnoozedForAlarm = new ArraySet<>();
  
      private AlarmManager mAlarmManager;
      private boolean mConnected;
      private boolean mRegistered;
      private long mNextAlarmTime;
  
      public ScheduleConditionProvider() {
          if (DEBUG) Slog.d(TAG, "new " + SIMPLE_NAME + "()");
      }
  
      @Override
      public ComponentName getComponent() {
          return COMPONENT;
      }
  
      @Override
      public boolean isValidConditionId(Uri id) {
          return ZenModeConfig.isValidScheduleConditionId(id);
      }
  
      @Override
      public void dump(PrintWriter pw, DumpFilter filter) {
          pw.print("    "); pw.print(SIMPLE_NAME); pw.println(":");
          pw.print("      mConnected="); pw.println(mConnected);
          pw.print("      mRegistered="); pw.println(mRegistered);
          pw.println("      mSubscriptions=");
          final long now = System.currentTimeMillis();
          synchronized (mSubscriptions) {
              for (Uri conditionId : mSubscriptions.keySet()) {
                  pw.print("        ");
                  pw.print(meetsSchedule(mSubscriptions.get(conditionId), now) ? "* " : "  ");
                  pw.println(conditionId);
                  pw.print("            ");
                  pw.println(mSubscriptions.get(conditionId).toString());
              }
          }
          pw.println("      snoozed due to alarm: " + TextUtils.join(SEPARATOR, mSnoozedForAlarm));
          dumpUpcomingTime(pw, "mNextAlarmTime", mNextAlarmTime, now);
      }
  
      @Override
      public void onConnected() {
          if (DEBUG) Slog.d(TAG, "onConnected");
          mConnected = true;
          readSnoozed();
      }
  
      @Override
      public void onBootComplete() {
          // noop
      }
  
      @Override
      public void onDestroy() {
          super.onDestroy();
          if (DEBUG) Slog.d(TAG, "onDestroy");
          mConnected = false;
      }
  
      @Override
      public void onSubscribe(Uri conditionId) {
          if (DEBUG) Slog.d(TAG, "onSubscribe " + conditionId);
          if (!ZenModeConfig.isValidScheduleConditionId(conditionId)) {
              notifyCondition(createCondition(conditionId, Condition.STATE_ERROR, "invalidId"));
              return;
          }
          synchronized (mSubscriptions) {
              mSubscriptions.put(conditionId, ZenModeConfig.toScheduleCalendar(conditionId));
          }
          evaluateSubscriptions();
      }
  
      @Override
      public void onUnsubscribe(Uri conditionId) {
          if (DEBUG) Slog.d(TAG, "onUnsubscribe " + conditionId);
          synchronized (mSubscriptions) {
              mSubscriptions.remove(conditionId);
          }
          removeSnoozed(conditionId);
          evaluateSubscriptions();
      }
  
      @Override
      public void attachBase(Context base) {
          attachBaseContext(base);
      }
  
      @Override
      public IConditionProvider asInterface() {
          return (IConditionProvider) onBind(null);
      }
  
      private void evaluateSubscriptions() {
          if (mAlarmManager == null) {
              mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
          }
          final long now = System.currentTimeMillis();
          mNextAlarmTime = 0;
          long nextUserAlarmTime = getNextAlarm();
          List<Condition> conditionsToNotify = new ArrayList<>();
          synchronized (mSubscriptions) {
              setRegistered(!mSubscriptions.isEmpty());
              for (Uri conditionId : mSubscriptions.keySet()) {
                  Condition condition =
                          evaluateSubscriptionLocked(conditionId, mSubscriptions.get(conditionId),
                                  now, nextUserAlarmTime);
                  if (condition != null) {
                      conditionsToNotify.add(condition);
                  }
              }
          }
          notifyConditions(conditionsToNotify.toArray(new Condition[conditionsToNotify.size()]));
          updateAlarm(now, mNextAlarmTime);
      }
  
      @VisibleForTesting
      @GuardedBy("mSubscriptions")
      Condition evaluateSubscriptionLocked(Uri conditionId, ScheduleCalendar cal,
              long now, long nextUserAlarmTime) {
          if (DEBUG) Slog.d(TAG, String.format("evaluateSubscriptionLocked cal=%s, now=%s, "
                          + "nextUserAlarmTime=%s", cal, ts(now), ts(nextUserAlarmTime)));
          Condition condition;
          if (cal == null) {
              condition = createCondition(conditionId, Condition.STATE_ERROR, "!invalidId");
              removeSnoozed(conditionId);
              return condition;
          }
          if (cal.isInSchedule(now)) {
              if (conditionSnoozed(conditionId)) {
                  condition = createCondition(conditionId, Condition.STATE_FALSE, "snoozed");
              } else if (cal.shouldExitForAlarm(now)) {
                  condition = createCondition(conditionId, Condition.STATE_FALSE, "alarmCanceled");
                  addSnoozed(conditionId);
              } else {
                  condition = createCondition(conditionId, Condition.STATE_TRUE, "meetsSchedule");
              }
          } else {
              condition = createCondition(conditionId, Condition.STATE_FALSE, "!meetsSchedule");
              removeSnoozed(conditionId);
          }
          cal.maybeSetNextAlarm(now, nextUserAlarmTime);
          final long nextChangeTime = cal.getNextChangeTime(now);
          if (nextChangeTime > 0 && nextChangeTime > now) {
              if (mNextAlarmTime == 0 || nextChangeTime < mNextAlarmTime) {
                  mNextAlarmTime = nextChangeTime;
              }
          }
          return condition;
      }
  
      private void updateAlarm(long now, long time) {
          final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
          final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext,
                  REQUEST_CODE_EVALUATE,
                  new Intent(ACTION_EVALUATE)
                          .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
                          .putExtra(EXTRA_TIME, time),
                  PendingIntent.FLAG_UPDATE_CURRENT);
          alarms.cancel(pendingIntent);
          if (time > now) {
              if (DEBUG) Slog.d(TAG, String.format("Scheduling evaluate for %s, in %s, now=%s",
                      ts(time), formatDuration(time - now), ts(now)));
              alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent);
          } else {
              if (DEBUG) Slog.d(TAG, "Not scheduling evaluate");
          }
      }
  
      public long getNextAlarm() {
          final AlarmManager.AlarmClockInfo info = mAlarmManager.getNextAlarmClock(
                  ActivityManager.getCurrentUser());
          return info != null ? info.getTriggerTime() : 0;
      }
  
      private boolean meetsSchedule(ScheduleCalendar cal, long time) {
          return cal != null && cal.isInSchedule(time);
      }
  
      private void setRegistered(boolean registered) {
          if (mRegistered == registered) return;
          if (DEBUG) Slog.d(TAG, "setRegistered " + registered);
          mRegistered = registered;
          if (mRegistered) {
              final IntentFilter filter = new IntentFilter();
              filter.addAction(Intent.ACTION_TIME_CHANGED);
              filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
              filter.addAction(ACTION_EVALUATE);
              filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED);
              registerReceiver(mReceiver, filter);
          } else {
              unregisterReceiver(mReceiver);
          }
      }
  
      private Condition createCondition(Uri id, int state, String reason) {
          if (DEBUG) Slog.d(TAG, "notifyCondition " + id
                  + " " + Condition.stateToString(state)
                  + " reason=" + reason);
          final String summary = NOT_SHOWN;
          final String line1 = NOT_SHOWN;
          final String line2 = NOT_SHOWN;
          return new Condition(id, summary, line1, line2, 0, state, Condition.FLAG_RELEVANT_ALWAYS);
      }
  
      private boolean conditionSnoozed(Uri conditionId) {
          synchronized (mSnoozedForAlarm) {
              return mSnoozedForAlarm.contains(conditionId);
          }
      }
  
      @VisibleForTesting
      void addSnoozed(Uri conditionId) {
          synchronized (mSnoozedForAlarm) {
              mSnoozedForAlarm.add(conditionId);
              saveSnoozedLocked();
          }
      }
  
      private void removeSnoozed(Uri conditionId) {
          synchronized (mSnoozedForAlarm) {
              mSnoozedForAlarm.remove(conditionId);
              saveSnoozedLocked();
          }
      }
  
      private void saveSnoozedLocked() {
          final String setting = TextUtils.join(SEPARATOR, mSnoozedForAlarm);
          final int currentUser = ActivityManager.getCurrentUser();
          Settings.Secure.putStringForUser(mContext.getContentResolver(),
                  SCP_SETTING,
                  setting,
                  currentUser);
      }
  
      private void readSnoozed() {
          synchronized (mSnoozedForAlarm) {
              long identity = Binder.clearCallingIdentity();
              try {
                  final String setting = Settings.Secure.getStringForUser(
                          mContext.getContentResolver(),
                          SCP_SETTING,
                          ActivityManager.getCurrentUser());
                  if (setting != null) {
                      final String[] tokens = setting.split(SEPARATOR);
                      for (int i = 0; i < tokens.length; i++) {
                          String token = tokens[i];
                          if (token != null) {
                              token = token.trim();
                          }
                          if (TextUtils.isEmpty(token)) {
                              continue;
                          }
                          mSnoozedForAlarm.add(Uri.parse(token));
                      }
                  }
              } finally {
                  Binder.restoreCallingIdentity(identity);
              }
          }
      }
  
      private BroadcastReceiver mReceiver = new BroadcastReceiver() {
          @Override
          public void onReceive(Context context, Intent intent) {
              if (DEBUG) Slog.d(TAG, "onReceive " + intent.getAction());
              if (Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) {
                  synchronized (mSubscriptions) {
                      for (Uri conditionId : mSubscriptions.keySet()) {
                          final ScheduleCalendar cal = mSubscriptions.get(conditionId);
                          if (cal != null) {
                              cal.setTimeZone(Calendar.getInstance().getTimeZone());
                          }
                      }
                  }
              }
              evaluateSubscriptions();
          }
      };
  
  }
  