/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.stetho.inspector.domstorage;
import android.content.Context;
import android.content.SharedPreferences;
import com.facebook.stetho.common.LogUtil;
import com.facebook.stetho.inspector.console.CLog;
import com.facebook.stetho.inspector.helper.ChromePeerManager;
import com.facebook.stetho.inspector.helper.PeerRegistrationListener;
import com.facebook.stetho.inspector.helper.PeersRegisteredListener;
import com.facebook.stetho.inspector.protocol.module.Console;
import com.facebook.stetho.inspector.protocol.module.DOMStorage;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class DOMStoragePeerManager extends ChromePeerManager {
private final Context mContext;
public DOMStoragePeerManager(Context context) {
mContext = context;
setListener(mPeerListener);
}
public void signalItemRemoved(DOMStorage.StorageId storageId, String key) {
DOMStorage.DomStorageItemRemovedParams params =
new DOMStorage.DomStorageItemRemovedParams();
params.storageId = storageId;
params.key = key;
sendNotificationToPeers("DOMStorage.domStorageItemRemoved", params);
}
public void signalItemAdded(DOMStorage.StorageId storageId, String key, String value) {
DOMStorage.DomStorageItemAddedParams params =
new DOMStorage.DomStorageItemAddedParams();
params.storageId = storageId;
params.key = key;
params.newValue = value;
sendNotificationToPeers("DOMStorage.domStorageItemAdded", params);
}
public void signalItemUpdated(
DOMStorage.StorageId storageId,
String key,
String oldValue,
String newValue) {
DOMStorage.DomStorageItemUpdatedParams params =
new DOMStorage.DomStorageItemUpdatedParams();
params.storageId = storageId;
params.key = key;
params.oldValue = oldValue;
params.newValue = newValue;
sendNotificationToPeers("DOMStorage.domStorageItemUpdated", params);
}
private final PeerRegistrationListener mPeerListener = new PeersRegisteredListener() {
private final List<DevToolsSharedPreferencesListener> mPrefsListeners =
new ArrayList<DevToolsSharedPreferencesListener>();
@Override
protected synchronized void onFirstPeerRegistered() {
// TODO: We list the tags in Page.getResourceTree as well and those are the real fixed
// tags that will be observed by the peer. We can fix this by making the page frames
// dynamically update in response to DOMStorage events. This would also allow us to
// add new SharedPreferences tags as we observe them being created by way of
// android.os.FileObserver.
List<String> tags = SharedPreferencesHelper.getSharedPreferenceTags(mContext);
for (String tag : tags) {
SharedPreferences prefs = mContext.getSharedPreferences(tag, Context.MODE_PRIVATE);
DevToolsSharedPreferencesListener listener =
new DevToolsSharedPreferencesListener(prefs, tag);
prefs.registerOnSharedPreferenceChangeListener(listener);
mPrefsListeners.add(listener);
}
}
@Override
protected synchronized void onLastPeerUnregistered() {
for (DevToolsSharedPreferencesListener prefsListener : mPrefsListeners) {
prefsListener.unregister();
}
mPrefsListeners.clear();
}
};
private class DevToolsSharedPreferencesListener
implements SharedPreferences.OnSharedPreferenceChangeListener {
private final SharedPreferences mPrefs;
private final DOMStorage.StorageId mStorageId;
/**
* Maintains a copy of the prefs data structure so that we can invoke
* {@code DOMStorage.domStorageItemUpdated}. This method requires that we know the old
* value to perform updates. Using {@code domStorageItemRemoved}/{@code Added} causes a UI
* glitch where the item is moved to the end of the list, unfortunately.
*/
private final Map<String, Object> mCopy;
public DevToolsSharedPreferencesListener(SharedPreferences prefs, String tag) {
mPrefs = prefs;
mStorageId = new DOMStorage.StorageId();
mStorageId.securityOrigin = tag;
mStorageId.isLocalStorage = true;
mCopy = prefsCopy(prefs.getAll());
}
public void unregister() {
mPrefs.unregisterOnSharedPreferenceChangeListener(this);
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
Map<String, ?> entries = sharedPreferences.getAll();
boolean existedBefore = mCopy.containsKey(key);
boolean existsNow = entries.containsKey(key);
Object newValue = existsNow ? entries.get(key) : null;
if (existedBefore && existsNow) {
signalItemUpdated(
mStorageId,
key,
SharedPreferencesHelper.valueToString(mCopy.get(key)),
SharedPreferencesHelper.valueToString(newValue));
mCopy.put(key, newValue);
} else if (existedBefore) {
signalItemRemoved(mStorageId, key);
mCopy.remove(key);
} else if (existsNow) {
signalItemAdded(
mStorageId,
key,
SharedPreferencesHelper.valueToString(newValue));
mCopy.put(key, newValue);
} else {
// This can happen due to the async nature of the onSharedPreferenceChanged callback. A
// rapid put/remove as two separate commits on a background thread would cause this.
LogUtil.i("Detected rapid put/remove of %s", key);
}
}
}
private static Map<String, Object> prefsCopy(Map<String, ?> src) {
HashMap<String, Object> dst = new HashMap<String, Object>(src.size());
for (Map.Entry<String, ?> entry : src.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (value instanceof Set) {
dst.put(key, shallowCopy((Set<String>)value));
} else {
dst.put(key, value);
}
}
return dst;
}
private static <T> Set<T> shallowCopy(Set<T> src) {
HashSet<T> dst = new HashSet<T>();
for (T item : src) {
dst.add(item);
}
return dst;
}
}