/*
* 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.protocol.module;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import com.facebook.stetho.inspector.console.CLog;
import com.facebook.stetho.inspector.domstorage.DOMStoragePeerManager;
import com.facebook.stetho.inspector.domstorage.SharedPreferencesHelper;
import com.facebook.stetho.inspector.jsonrpc.JsonRpcException;
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 java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
public class DOMStorage implements ChromeDevtoolsDomain {
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, JsonRpcException {
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 existingValue = prefs.getAll().get(key);
try {
if (existingValue == null) {
throw new DOMStorageAssignmentException(
"Unsupported: cannot add new key " + key + " due to lack of type inference");
} else {
SharedPreferences.Editor editor = prefs.edit();
try {
assignByType(editor, key, SharedPreferencesHelper.valueFromString(value, existingValue));
editor.apply();
} catch (IllegalArgumentException e) {
throw new DOMStorageAssignmentException(
String.format(Locale.US,
"Type mismatch setting %s to %s (expected %s)",
key,
value,
existingValue.getClass().getSimpleName()));
}
}
} catch (DOMStorageAssignmentException e) {
CLog.writeToConsole(
mDOMStoragePeerManager,
Console.MessageLevel.ERROR,
Console.MessageSource.STORAGE,
e.getMessage());
// Force the DevTools UI to refresh with the old value again (it assumes that the set
// operation succeeded). Note that we should be able to do this by throwing
// JsonRpcException but the UI doesn't respect setDOMStorageItem failure.
if (prefs.contains(key)) {
mDOMStoragePeerManager.signalItemUpdated(
storage,
key,
value,
SharedPreferencesHelper.valueToString(existingValue));
} else {
mDOMStoragePeerManager.signalItemRemoved(storage, key);
}
}
}
}
@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 void assignByType(
SharedPreferences.Editor editor,
String key,
Object value)
throws IllegalArgumentException {
if (value instanceof Integer) {
editor.putInt(key, (Integer)value);
} else if (value instanceof Long) {
editor.putLong(key, (Long)value);
} else if (value instanceof Float) {
editor.putFloat(key, (Float)value);
} else if (value instanceof Boolean) {
editor.putBoolean(key, (Boolean)value);
} else if (value instanceof String) {
editor.putString(key, (String)value);
} else if (value instanceof Set) {
putStringSet(editor, key, (Set<String>)value);
} else {
throw new IllegalArgumentException("Unsupported type=" + value.getClass().getName());
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private static void putStringSet(SharedPreferences.Editor editor, String key, Set<String> value) {
editor.putStringSet(key, value);
}
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;
}
/**
* Exception thrown internally when we fail to honor {@link #setDOMStorageItem}.
*/
private static class DOMStorageAssignmentException extends Exception {
public DOMStorageAssignmentException(String message) {
super(message);
}
}
}