19f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li/*
2f04335f899f2cce69f843692a3cb9cec229683c2tturney * Copyright (C) 2017 The Android Open Source Project
39f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li *
4f04335f899f2cce69f843692a3cb9cec229683c2tturney * Licensed under the Apache License, Version 2.0 (the "License");
5f04335f899f2cce69f843692a3cb9cec229683c2tturney * you may not use this file except in compliance with the License.
6f04335f899f2cce69f843692a3cb9cec229683c2tturney * You may obtain a copy of the License at
79f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li *
8f04335f899f2cce69f843692a3cb9cec229683c2tturney *      http://www.apache.org/licenses/LICENSE-2.0
99f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li *
109f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li * Unless required by applicable law or agreed to in writing, software
11f04335f899f2cce69f843692a3cb9cec229683c2tturney * distributed under the License is distributed on an "AS IS" BASIS,
12f04335f899f2cce69f843692a3cb9cec229683c2tturney * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13f04335f899f2cce69f843692a3cb9cec229683c2tturney * See the License for the specific language governing permissions and
14f04335f899f2cce69f843692a3cb9cec229683c2tturney * limitations under the License.
159f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li */
169f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li
179f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Lipackage com.googlecode.android_scripting.trigger;
189f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li
199f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Liimport android.content.Context;
209f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Liimport android.content.Intent;
219f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Liimport android.content.SharedPreferences;
229f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Liimport android.preference.PreferenceManager;
239f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li
249f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Liimport com.google.common.collect.ArrayListMultimap;
259f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Liimport com.google.common.collect.Multimap;
269f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Liimport com.google.common.collect.Multimaps;
279f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Liimport com.googlecode.android_scripting.IntentBuilders;
289f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Liimport com.googlecode.android_scripting.Log;
299f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li
309f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Liimport java.io.ByteArrayInputStream;
319f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Liimport java.io.ByteArrayOutputStream;
329f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Liimport java.io.IOException;
339f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Liimport java.io.ObjectInputStream;
349f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Liimport java.io.ObjectOutputStream;
359f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Liimport java.util.Map.Entry;
369f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Liimport java.util.concurrent.CopyOnWriteArrayList;
379f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li
389f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Liimport org.apache.commons.codec.binary.Base64Codec;
399f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li
409f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li/**
419f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li * A repository maintaining all currently scheduled triggers. This includes, for example, alarms or
429f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li * observers of arriving text messages etc. This class is responsible for serializing the list of
439f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li * triggers to the shared preferences store, and retrieving it from there.
449f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li *
459f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li */
469f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Lipublic class TriggerRepository {
479f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  /**
489f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li   * The list of triggers is serialized to the shared preferences entry with this name.
499f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li   */
509f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  private static final String TRIGGERS_PREF_KEY = "TRIGGERS";
519f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li
529f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  private final SharedPreferences mPreferences;
539f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  private final Context mContext;
549f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li
559f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  /**
569f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li   * An interface for objects that are notified when a trigger is added to the repository.
579f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li   */
589f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  public interface TriggerRepositoryObserver {
599f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    /**
609f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li     * Invoked just before the trigger is added to the repository.
619f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li     *
629f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li     * @param trigger
639f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li     *          The trigger about to be added to the repository.
649f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li     */
659f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    void onPut(Trigger trigger);
669f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li
679f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    /**
689f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li     * Invoked just after the trigger has been removed from the repository.
699f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li     *
709f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li     * @param trigger
719f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li     *          The trigger that has just been removed from the repository.
729f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li     */
739f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    void onRemove(Trigger trigger);
749f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  }
759f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li
769f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  private final Multimap<String, Trigger> mTriggers;
779f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  private final CopyOnWriteArrayList<TriggerRepositoryObserver> mTriggerObservers =
789f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li      new CopyOnWriteArrayList<TriggerRepositoryObserver>();
799f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li
809f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  public TriggerRepository(Context context) {
819f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    mContext = context;
829f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    mPreferences = PreferenceManager.getDefaultSharedPreferences(context);
839f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    String triggers = mPreferences.getString(TRIGGERS_PREF_KEY, null);
849f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    mTriggers = deserializeTriggersFromString(triggers);
859f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  }
869f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li
879f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  /** Returns a list of all triggers. The list is unmodifiable. */
889f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  public synchronized Multimap<String, Trigger> getAllTriggers() {
899f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    return Multimaps.unmodifiableMultimap(mTriggers);
909f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  }
919f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li
929f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  /**
939f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li   * Adds a new trigger to the repository.
949f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li   *
959f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li   * @param trigger
969f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li   *          the {@link Trigger} to add
979f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li   */
989f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  public synchronized void put(Trigger trigger) {
999f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    notifyOnAdd(trigger);
1009f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    mTriggers.put(trigger.getEventName(), trigger);
1019f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    storeTriggers();
1029f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    ensureTriggerServiceRunning();
1039f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  }
1049f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li
1059f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  /** Removes a specific {@link Trigger}. */
1069f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  public synchronized void remove(final Trigger trigger) {
1079f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    mTriggers.get(trigger.getEventName()).remove(trigger);
1089f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    storeTriggers();
1099f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    notifyOnRemove(trigger);
1109f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  }
1119f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li
1129f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  /** Ensures that the {@link TriggerService} is running */
1139f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  private void ensureTriggerServiceRunning() {
1149f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    Intent startTriggerServiceIntent = IntentBuilders.buildTriggerServiceIntent();
1159f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    mContext.startService(startTriggerServiceIntent);
1169f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  }
1179f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li
1189f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  /** Notify all {@link TriggerRepositoryObserver}s that a {@link Trigger} was added. */
1199f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  private void notifyOnAdd(Trigger trigger) {
1209f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    for (TriggerRepositoryObserver observer : mTriggerObservers) {
1219f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li      observer.onPut(trigger);
1229f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    }
1239f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  }
1249f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li
1259f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  /** Notify all {@link TriggerRepositoryObserver}s that a {@link Trigger} was removed. */
1269f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  private void notifyOnRemove(Trigger trigger) {
1279f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    for (TriggerRepositoryObserver observer : mTriggerObservers) {
1289f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li      observer.onRemove(trigger);
1299f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    }
1309f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  }
1319f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li
1329f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  /** Writes the list of triggers to the shared preferences. */
1339f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  private synchronized void storeTriggers() {
1349f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    SharedPreferences.Editor editor = mPreferences.edit();
1359f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    final String triggerValue = serializeTriggersToString(mTriggers);
1369f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    if (triggerValue != null) {
1379f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li      editor.putString(TRIGGERS_PREF_KEY, triggerValue);
1389f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    }
1399f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    editor.commit();
1409f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  }
1419f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li
1429f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  /** Deserializes the {@link Multimap} of {@link Trigger}s from a base 64 encoded string. */
1439f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  @SuppressWarnings("unchecked")
1449f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  private Multimap<String, Trigger> deserializeTriggersFromString(String triggers) {
1459f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    if (triggers == null) {
1469f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li      return ArrayListMultimap.<String, Trigger> create();
1479f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    }
1489f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    try {
1499f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li      final ByteArrayInputStream inputStream =
1509f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li          new ByteArrayInputStream(Base64Codec.decodeBase64(triggers.getBytes()));
1519f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li      final ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
1529f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li      return (Multimap<String, Trigger>) objectInputStream.readObject();
1539f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    } catch (Exception e) {
1549f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li      Log.e(e);
1559f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    }
1569f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    return ArrayListMultimap.<String, Trigger> create();
1579f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  }
1589f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li
1599f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  /** Serializes the list of triggers to a Base64 encoded string. */
1609f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  private String serializeTriggersToString(Multimap<String, Trigger> triggers) {
1619f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    try {
1629f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li      final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
1639f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li      final ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
1649f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li      objectOutputStream.writeObject(triggers);
1659f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li      return new String(Base64Codec.encodeBase64(outputStream.toByteArray()));
1669f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    } catch (IOException e) {
1679f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li      Log.e(e);
1689f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li      return null;
1699f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    }
1709f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  }
1719f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li
1729f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  /** Returns {@code true} iff the list of triggers is empty. */
1739f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  public synchronized boolean isEmpty() {
1749f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    return mTriggers.isEmpty();
1759f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  }
1769f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li
1779f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  /** Adds a {@link TriggerRepositoryObserver}. */
1789f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  public void addObserver(TriggerRepositoryObserver observer) {
1799f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    mTriggerObservers.add(observer);
1809f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  }
1819f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li
1829f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  /**
1839f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li   * Adds the given {@link TriggerRepositoryObserver} and invokes
1849f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li   * {@link TriggerRepositoryObserver#onPut} for all existing triggers.
1859f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li   *
1869f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li   * @param observer
1879f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li   *          The observer to add.
1889f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li   */
1899f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  public synchronized void bootstrapObserver(TriggerRepositoryObserver observer) {
1909f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    addObserver(observer);
1919f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    for (Entry<String, Trigger> trigger : mTriggers.entries()) {
1929f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li      observer.onPut(trigger.getValue());
1939f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    }
1949f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  }
1959f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li
1969f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  /**
1979f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li   * Removes a {@link TriggerRepositoryObserver}.
1989f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li   */
1999f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  public void removeObserver(TriggerRepositoryObserver observer) {
2009f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li    mTriggerObservers.remove(observer);
2019f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li  }
2029f32db87b486c93a0ea71eb1781ee45676b8bf8bXin Li}