1 /* 2 * Copyright (C) 2016 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17 package com.googlecode.android_scripting.trigger; 18 19 import android.content.Context; 20 import android.content.Intent; 21 import android.content.SharedPreferences; 22 import android.preference.PreferenceManager; 23 24 import com.google.common.collect.ArrayListMultimap; 25 import com.google.common.collect.Multimap; 26 import com.google.common.collect.Multimaps; 27 import com.googlecode.android_scripting.IntentBuilders; 28 import com.googlecode.android_scripting.Log; 29 30 import java.io.ByteArrayInputStream; 31 import java.io.ByteArrayOutputStream; 32 import java.io.IOException; 33 import java.io.ObjectInputStream; 34 import java.io.ObjectOutputStream; 35 import java.util.Map.Entry; 36 import java.util.concurrent.CopyOnWriteArrayList; 37 38 import org.apache.commons.codec.binary.Base64Codec; 39 40 /** 41 * A repository maintaining all currently scheduled triggers. This includes, for example, alarms or 42 * observers of arriving text messages etc. This class is responsible for serializing the list of 43 * triggers to the shared preferences store, and retrieving it from there. 44 * 45 * @author Felix Arends (felix.arends (at) gmail.com) 46 * @author Damon Kohler (damonkohler (at) gmail.com) 47 */ 48 public class TriggerRepository { 49 /** 50 * The list of triggers is serialized to the shared preferences entry with this name. 51 */ 52 private static final String TRIGGERS_PREF_KEY = "TRIGGERS"; 53 54 private final SharedPreferences mPreferences; 55 private final Context mContext; 56 57 /** 58 * An interface for objects that are notified when a trigger is added to the repository. 59 */ 60 public interface TriggerRepositoryObserver { 61 /** 62 * Invoked just before the trigger is added to the repository. 63 * 64 * @param trigger 65 * The trigger about to be added to the repository. 66 */ 67 void onPut(Trigger trigger); 68 69 /** 70 * Invoked just after the trigger has been removed from the repository. 71 * 72 * @param trigger 73 * The trigger that has just been removed from the repository. 74 */ 75 void onRemove(Trigger trigger); 76 } 77 78 private final Multimap<String, Trigger> mTriggers; 79 private final CopyOnWriteArrayList<TriggerRepositoryObserver> mTriggerObservers = 80 new CopyOnWriteArrayList<TriggerRepositoryObserver>(); 81 82 public TriggerRepository(Context context) { 83 mContext = context; 84 mPreferences = PreferenceManager.getDefaultSharedPreferences(context); 85 String triggers = mPreferences.getString(TRIGGERS_PREF_KEY, null); 86 mTriggers = deserializeTriggersFromString(triggers); 87 } 88 89 /** Returns a list of all triggers. The list is unmodifiable. */ 90 public synchronized Multimap<String, Trigger> getAllTriggers() { 91 return Multimaps.unmodifiableMultimap(mTriggers); 92 } 93 94 /** 95 * Adds a new trigger to the repository. 96 * 97 * @param trigger 98 * the {@link Trigger} to add 99 */ 100 public synchronized void put(Trigger trigger) { 101 notifyOnAdd(trigger); 102 mTriggers.put(trigger.getEventName(), trigger); 103 storeTriggers(); 104 ensureTriggerServiceRunning(); 105 } 106 107 /** Removes a specific {@link Trigger}. */ 108 public synchronized void remove(final Trigger trigger) { 109 mTriggers.get(trigger.getEventName()).remove(trigger); 110 storeTriggers(); 111 notifyOnRemove(trigger); 112 } 113 114 /** Ensures that the {@link TriggerService} is running */ 115 private void ensureTriggerServiceRunning() { 116 Intent startTriggerServiceIntent = IntentBuilders.buildTriggerServiceIntent(); 117 mContext.startService(startTriggerServiceIntent); 118 } 119 120 /** Notify all {@link TriggerRepositoryObserver}s that a {@link Trigger} was added. */ 121 private void notifyOnAdd(Trigger trigger) { 122 for (TriggerRepositoryObserver observer : mTriggerObservers) { 123 observer.onPut(trigger); 124 } 125 } 126 127 /** Notify all {@link TriggerRepositoryObserver}s that a {@link Trigger} was removed. */ 128 private void notifyOnRemove(Trigger trigger) { 129 for (TriggerRepositoryObserver observer : mTriggerObservers) { 130 observer.onRemove(trigger); 131 } 132 } 133 134 /** Writes the list of triggers to the shared preferences. */ 135 private synchronized void storeTriggers() { 136 SharedPreferences.Editor editor = mPreferences.edit(); 137 final String triggerValue = serializeTriggersToString(mTriggers); 138 if (triggerValue != null) { 139 editor.putString(TRIGGERS_PREF_KEY, triggerValue); 140 } 141 editor.commit(); 142 } 143 144 /** Deserializes the {@link Multimap} of {@link Trigger}s from a base 64 encoded string. */ 145 @SuppressWarnings("unchecked") 146 private Multimap<String, Trigger> deserializeTriggersFromString(String triggers) { 147 if (triggers == null) { 148 return ArrayListMultimap.<String, Trigger> create(); 149 } 150 try { 151 final ByteArrayInputStream inputStream = 152 new ByteArrayInputStream(Base64Codec.decodeBase64(triggers.getBytes())); 153 final ObjectInputStream objectInputStream = new ObjectInputStream(inputStream); 154 return (Multimap<String, Trigger>) objectInputStream.readObject(); 155 } catch (Exception e) { 156 Log.e(e); 157 } 158 return ArrayListMultimap.<String, Trigger> create(); 159 } 160 161 /** Serializes the list of triggers to a Base64 encoded string. */ 162 private String serializeTriggersToString(Multimap<String, Trigger> triggers) { 163 try { 164 final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 165 final ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream); 166 objectOutputStream.writeObject(triggers); 167 return new String(Base64Codec.encodeBase64(outputStream.toByteArray())); 168 } catch (IOException e) { 169 Log.e(e); 170 return null; 171 } 172 } 173 174 /** Returns {@code true} iff the list of triggers is empty. */ 175 public synchronized boolean isEmpty() { 176 return mTriggers.isEmpty(); 177 } 178 179 /** Adds a {@link TriggerRepositoryObserver}. */ 180 public void addObserver(TriggerRepositoryObserver observer) { 181 mTriggerObservers.add(observer); 182 } 183 184 /** 185 * Adds the given {@link TriggerRepositoryObserver} and invokes 186 * {@link TriggerRepositoryObserver#onPut} for all existing triggers. 187 * 188 * @param observer 189 * The observer to add. 190 */ 191 public synchronized void bootstrapObserver(TriggerRepositoryObserver observer) { 192 addObserver(observer); 193 for (Entry<String, Trigger> trigger : mTriggers.entries()) { 194 observer.onPut(trigger.getValue()); 195 } 196 } 197 198 /** 199 * Removes a {@link TriggerRepositoryObserver}. 200 */ 201 public void removeObserver(TriggerRepositoryObserver observer) { 202 mTriggerObservers.remove(observer); 203 } 204 }