1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of 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, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under 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 */ 46 public class TriggerRepository { 47 /** 48 * The list of triggers is serialized to the shared preferences entry with this name. 49 */ 50 private static final String TRIGGERS_PREF_KEY = "TRIGGERS"; 51 52 private final SharedPreferences mPreferences; 53 private final Context mContext; 54 55 /** 56 * An interface for objects that are notified when a trigger is added to the repository. 57 */ 58 public interface TriggerRepositoryObserver { 59 /** 60 * Invoked just before the trigger is added to the repository. 61 * 62 * @param trigger 63 * The trigger about to be added to the repository. 64 */ onPut(Trigger trigger)65 void onPut(Trigger trigger); 66 67 /** 68 * Invoked just after the trigger has been removed from the repository. 69 * 70 * @param trigger 71 * The trigger that has just been removed from the repository. 72 */ onRemove(Trigger trigger)73 void onRemove(Trigger trigger); 74 } 75 76 private final Multimap<String, Trigger> mTriggers; 77 private final CopyOnWriteArrayList<TriggerRepositoryObserver> mTriggerObservers = 78 new CopyOnWriteArrayList<TriggerRepositoryObserver>(); 79 TriggerRepository(Context context)80 public TriggerRepository(Context context) { 81 mContext = context; 82 mPreferences = PreferenceManager.getDefaultSharedPreferences(context); 83 String triggers = mPreferences.getString(TRIGGERS_PREF_KEY, null); 84 mTriggers = deserializeTriggersFromString(triggers); 85 } 86 87 /** Returns a list of all triggers. The list is unmodifiable. */ getAllTriggers()88 public synchronized Multimap<String, Trigger> getAllTriggers() { 89 return Multimaps.unmodifiableMultimap(mTriggers); 90 } 91 92 /** 93 * Adds a new trigger to the repository. 94 * 95 * @param trigger 96 * the {@link Trigger} to add 97 */ put(Trigger trigger)98 public synchronized void put(Trigger trigger) { 99 notifyOnAdd(trigger); 100 mTriggers.put(trigger.getEventName(), trigger); 101 storeTriggers(); 102 ensureTriggerServiceRunning(); 103 } 104 105 /** Removes a specific {@link Trigger}. */ remove(final Trigger trigger)106 public synchronized void remove(final Trigger trigger) { 107 mTriggers.get(trigger.getEventName()).remove(trigger); 108 storeTriggers(); 109 notifyOnRemove(trigger); 110 } 111 112 /** Ensures that the {@link TriggerService} is running */ ensureTriggerServiceRunning()113 private void ensureTriggerServiceRunning() { 114 Intent startTriggerServiceIntent = IntentBuilders.buildTriggerServiceIntent(); 115 mContext.startService(startTriggerServiceIntent); 116 } 117 118 /** Notify all {@link TriggerRepositoryObserver}s that a {@link Trigger} was added. */ notifyOnAdd(Trigger trigger)119 private void notifyOnAdd(Trigger trigger) { 120 for (TriggerRepositoryObserver observer : mTriggerObservers) { 121 observer.onPut(trigger); 122 } 123 } 124 125 /** Notify all {@link TriggerRepositoryObserver}s that a {@link Trigger} was removed. */ notifyOnRemove(Trigger trigger)126 private void notifyOnRemove(Trigger trigger) { 127 for (TriggerRepositoryObserver observer : mTriggerObservers) { 128 observer.onRemove(trigger); 129 } 130 } 131 132 /** Writes the list of triggers to the shared preferences. */ storeTriggers()133 private synchronized void storeTriggers() { 134 SharedPreferences.Editor editor = mPreferences.edit(); 135 final String triggerValue = serializeTriggersToString(mTriggers); 136 if (triggerValue != null) { 137 editor.putString(TRIGGERS_PREF_KEY, triggerValue); 138 } 139 editor.commit(); 140 } 141 142 /** Deserializes the {@link Multimap} of {@link Trigger}s from a base 64 encoded string. */ 143 @SuppressWarnings("unchecked") deserializeTriggersFromString(String triggers)144 private Multimap<String, Trigger> deserializeTriggersFromString(String triggers) { 145 if (triggers == null) { 146 return ArrayListMultimap.<String, Trigger> create(); 147 } 148 try { 149 final ByteArrayInputStream inputStream = 150 new ByteArrayInputStream(Base64Codec.decodeBase64(triggers.getBytes())); 151 final ObjectInputStream objectInputStream = new ObjectInputStream(inputStream); 152 return (Multimap<String, Trigger>) objectInputStream.readObject(); 153 } catch (Exception e) { 154 Log.e(e); 155 } 156 return ArrayListMultimap.<String, Trigger> create(); 157 } 158 159 /** Serializes the list of triggers to a Base64 encoded string. */ serializeTriggersToString(Multimap<String, Trigger> triggers)160 private String serializeTriggersToString(Multimap<String, Trigger> triggers) { 161 try { 162 final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 163 final ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream); 164 objectOutputStream.writeObject(triggers); 165 return new String(Base64Codec.encodeBase64(outputStream.toByteArray())); 166 } catch (IOException e) { 167 Log.e(e); 168 return null; 169 } 170 } 171 172 /** Returns {@code true} iff the list of triggers is empty. */ isEmpty()173 public synchronized boolean isEmpty() { 174 return mTriggers.isEmpty(); 175 } 176 177 /** Adds a {@link TriggerRepositoryObserver}. */ addObserver(TriggerRepositoryObserver observer)178 public void addObserver(TriggerRepositoryObserver observer) { 179 mTriggerObservers.add(observer); 180 } 181 182 /** 183 * Adds the given {@link TriggerRepositoryObserver} and invokes 184 * {@link TriggerRepositoryObserver#onPut} for all existing triggers. 185 * 186 * @param observer 187 * The observer to add. 188 */ bootstrapObserver(TriggerRepositoryObserver observer)189 public synchronized void bootstrapObserver(TriggerRepositoryObserver observer) { 190 addObserver(observer); 191 for (Entry<String, Trigger> trigger : mTriggers.entries()) { 192 observer.onPut(trigger.getValue()); 193 } 194 } 195 196 /** 197 * Removes a {@link TriggerRepositoryObserver}. 198 */ removeObserver(TriggerRepositoryObserver observer)199 public void removeObserver(TriggerRepositoryObserver observer) { 200 mTriggerObservers.remove(observer); 201 } 202 }