Skip to content

Commit

Permalink
Add SharedPreferences data to Resources tab
Browse files Browse the repository at this point in the history
This implements the "Local Storage" section of the Resources tab and
populates one entry per SharedPreferences tag discovered at the time the
inspector window is opened.  Full support for dynamic updates of
individual keys, and updating/inserting/deleting values.

Note that discovery of new SharedPreferences tags, just like new
SQLite databases, is not currently supported.  In the future, this can
be added using Android's FileObserver API.
  • Loading branch information
Josh Guilfoyle authored and longinoa committed Mar 11, 2015
1 parent c2c8440 commit 2274f38
Show file tree
Hide file tree
Showing 5 changed files with 348 additions and 15 deletions.
2 changes: 1 addition & 1 deletion stetho/src/main/java/com/facebook/stetho/Stetho.java
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ public Iterable<ChromeDevtoolsDomain> get() {
modules.add(new CSS());
modules.add(new Debugger());
modules.add(new DOM());
modules.add(new DOMStorage());
modules.add(new DOMStorage(context));
modules.add(new HeapProfiler());
modules.add(new Inspector());
modules.add(new Network(context));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.facebook.stetho.inspector.domstorage;

import android.content.Context;
import android.content.SharedPreferences;
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.DOMStorage;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class DOMStoragePeerManager extends ChromePeerManager {
private final Context mContext;

public DOMStoragePeerManager(Context context) {
mContext = context;
setListener(mPeerListener);
}

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;

public DevToolsSharedPreferencesListener(SharedPreferences prefs, String tag) {
mPrefs = prefs;
mStorageId = new DOMStorage.StorageId();
mStorageId.securityOrigin = tag;
mStorageId.isLocalStorage = true;
}

public void unregister() {
mPrefs.unregisterOnSharedPreferenceChangeListener(this);
}

@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
DOMStorage.DomStorageItemRemovedParams removedParams =
new DOMStorage.DomStorageItemRemovedParams();
removedParams.storageId = mStorageId;
removedParams.key = key;
sendNotificationToPeers("DOMStorage.domStorageItemRemoved", removedParams);

Map<String, ?> entries = sharedPreferences.getAll();
if (entries.containsKey(key)) {
DOMStorage.DomStorageItemAddedParams addedParams =
new DOMStorage.DomStorageItemAddedParams();
addedParams.storageId = mStorageId;
addedParams.key = key;
addedParams.newValue = SharedPreferencesHelper.valueToString(entries.get(key));
sendNotificationToPeers("DOMStorage.domStorageItemAdded", addedParams);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.facebook.stetho.inspector.domstorage;

import android.content.Context;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

public class SharedPreferencesHelper {
private static final String PREFS_SUFFIX = ".xml";

private SharedPreferencesHelper() {
}

public static List<String> getSharedPreferenceTags(Context context) {
ArrayList<String> tags = new ArrayList<String>();

String rootPath = context.getApplicationInfo().dataDir + "/shared_prefs";
File root = new File(rootPath);
if (root.exists()) {
for (File file : root.listFiles()) {
String fileName = file.getName();
if (fileName.endsWith(PREFS_SUFFIX)) {
tags.add(fileName.substring(0, fileName.length() - PREFS_SUFFIX.length()));
}
}
}

return tags;
}

public static String valueToString(Object value) {
if (value != null) {
return value.toString();
} else {
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,188 @@

package com.facebook.stetho.inspector.protocol.module;

import android.content.Context;
import android.content.SharedPreferences;
import com.facebook.stetho.common.LogUtil;
import com.facebook.stetho.inspector.domstorage.DOMStoragePeerManager;
import com.facebook.stetho.inspector.domstorage.SharedPreferencesHelper;
import com.facebook.stetho.inspector.jsonrpc.JsonRpcPeer;
import com.facebook.stetho.inspector.jsonrpc.JsonRpcResult;
import com.facebook.stetho.inspector.protocol.ChromeDevtoolsDomain;
import com.facebook.stetho.inspector.protocol.ChromeDevtoolsMethod;

import com.facebook.stetho.json.ObjectMapper;
import com.facebook.stetho.json.annotation.JsonProperty;
import org.json.JSONException;
import org.json.JSONObject;

import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class DOMStorage implements ChromeDevtoolsDomain {
public DOMStorage() {
private final Context mContext;
private final DOMStoragePeerManager mDOMStoragePeerManager;
private final ObjectMapper mObjectMapper = new ObjectMapper();

public DOMStorage(Context context) {
mContext = context;
mDOMStoragePeerManager = new DOMStoragePeerManager(context);
}

@ChromeDevtoolsMethod
public void enable(JsonRpcPeer peer, JSONObject params) {
mDOMStoragePeerManager.addPeer(peer);
}

@ChromeDevtoolsMethod
public void disable(JsonRpcPeer peer, JSONObject params) {
mDOMStoragePeerManager.removePeer(peer);
}

@ChromeDevtoolsMethod
public JsonRpcResult getDOMStorageItems(JsonRpcPeer peer, JSONObject params)
throws JSONException {
StorageId storage = mObjectMapper.convertValue(
params.getJSONObject("storageId"),
StorageId.class);

ArrayList<List<String>> entries = new ArrayList<List<String>>();
String prefTag = storage.securityOrigin;
if (storage.isLocalStorage) {
SharedPreferences prefs = mContext.getSharedPreferences(prefTag, Context.MODE_PRIVATE);
for (Map.Entry<String, ?> prefsEntry : prefs.getAll().entrySet()) {
ArrayList<String> entry = new ArrayList<String>(2);
entry.add(prefsEntry.getKey());
entry.add(SharedPreferencesHelper.valueToString(prefsEntry.getValue()));
entries.add(entry);
}
}

GetDOMStorageItemsResult result = new GetDOMStorageItemsResult();
result.entries = entries;
return result;
}

@ChromeDevtoolsMethod
public void setDOMStorageItem(JsonRpcPeer peer, JSONObject params) throws JSONException {
StorageId storage = mObjectMapper.convertValue(
params.getJSONObject("storageId"),
StorageId.class);
String key = params.getString("key");
String value = params.getString("value");

if (storage.isLocalStorage) {
SharedPreferences prefs = mContext.getSharedPreferences(
storage.securityOrigin,
Context.MODE_PRIVATE);
Object exitingValue = prefs.getAll().get(key);
SharedPreferences.Editor editor = prefs.edit();
if (!tryAssignByType(editor, key, value, exitingValue)) {
editor.putString(key, value);
}
editor.apply();
}
}

@ChromeDevtoolsMethod
public void removeDOMStorageItem(JsonRpcPeer peer, JSONObject params) throws JSONException {
StorageId storage = mObjectMapper.convertValue(
params.getJSONObject("storageId"),
StorageId.class);
String key = params.getString("key");

if (storage.isLocalStorage) {
SharedPreferences prefs = mContext.getSharedPreferences(
storage.securityOrigin,
Context.MODE_PRIVATE);
prefs.edit().remove(key).apply();
}
}

private static boolean tryAssignByType(
SharedPreferences.Editor editor,
String key,
String value,
@Nullable Object existingValue) {
try {
if (existingValue instanceof Integer) {
editor.putInt(key, Integer.parseInt(value));
return true;
} else if (existingValue instanceof Long) {
editor.putLong(key, Long.parseLong(value));
return true;
} else if (existingValue instanceof Float) {
editor.putFloat(key, Float.parseFloat(value));
return true;
} else if (existingValue instanceof Boolean) {
editor.putBoolean(key, parseBoolean(value));
return true;
}
} catch (NumberFormatException e) {
// Fall through...
}
return false;
}

private static Boolean parseBoolean(String s) {
if ("1".equals(s) || "true".equalsIgnoreCase(s)) {
return Boolean.TRUE;
} else if ("0".equals(s) || "false".equalsIgnoreCase(s)) {
return Boolean.FALSE;
}
return null;
}

public static class StorageId {
@JsonProperty(required = true)
public String securityOrigin;

@JsonProperty(required = true)
public boolean isLocalStorage;
}

private static class GetDOMStorageItemsResult implements JsonRpcResult {
@JsonProperty(required = true)
public List<List<String>> entries;
}

public static class DomStorageItemsClearedParams {
@JsonProperty(required = true)
public StorageId storageId;
}

public static class DomStorageItemRemovedParams {
@JsonProperty(required = true)
public StorageId storageId;

@JsonProperty(required = true)
public String key;
}

public static class DomStorageItemAddedParams {
@JsonProperty(required = true)
public StorageId storageId;

@JsonProperty(required = true)
public String key;

@JsonProperty(required = true)
public String newValue;
}

public static class DomStorageItemUpdatedParams {
@JsonProperty(required = true)
public StorageId storageId;

@JsonProperty(required = true)
public String key;

@JsonProperty(required = true)
public String oldValue;

@JsonProperty(required = true)
public String newValue;
}
}
Loading

0 comments on commit 2274f38

Please sign in to comment.