/*
* 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.dumpapp.plugins;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.text.TextUtils;
import com.facebook.stetho.dumpapp.DumpUsageException;
import com.facebook.stetho.dumpapp.DumperContext;
import com.facebook.stetho.dumpapp.DumperPlugin;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.File;
import java.io.PrintStream;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
public class SharedPreferencesDumperPlugin implements DumperPlugin {
private static final String XML_SUFFIX = ".xml";
private static final String NAME = "prefs";
private final Context mAppContext;
public SharedPreferencesDumperPlugin(Context context) {
mAppContext = context.getApplicationContext();
}
@Override
public String getName() {
return NAME;
}
@Override
public void dump(DumperContext dumpContext) throws DumpUsageException {
PrintStream writer = dumpContext.getStdout();
List<String> args = dumpContext.getArgsAsList();
String commandName = args.isEmpty() ? "" : args.remove(0);
if (commandName.equals("print")) {
doPrint(writer, args);
} else if (commandName.equals("write")) {
doWrite(args);
} else {
doUsage(writer);
}
}
/**
* Executes command to update one value in the shared preferences
*/
// We explicitly want commit() so that the dumper blocks while the write occurs.
@SuppressLint("CommitPrefEdits")
private void doWrite(List<String> args) throws DumpUsageException {
String usagePrefix = "Usage: prefs write <path> <key> <type> <value>, where type is one of: ";
Iterator<String> argsIter = args.iterator();
String path = nextArg(argsIter, "Expected <path>");
String key = nextArg(argsIter, "Expected <key>");
String typeName = nextArg(argsIter, "Expected <type>");
Type type = Type.of(typeName);
if (type == null) {
throw new DumpUsageException(
Type.appendNamesList(new StringBuilder(usagePrefix), ", ").toString());
}
SharedPreferences sharedPreferences = getSharedPreferences(path);
SharedPreferences.Editor editor = sharedPreferences.edit();
switch (type) {
case BOOLEAN:
editor.putBoolean(key, Boolean.valueOf(nextArgValue(argsIter)));
break;
case INT:
editor.putInt(key, Integer.valueOf(nextArgValue(argsIter)));
break;
case LONG:
editor.putLong(key, Long.valueOf(nextArgValue(argsIter)));
break;
case FLOAT:
editor.putFloat(key, Float.valueOf(nextArgValue(argsIter)));
break;
case STRING:
editor.putString(key, nextArgValue(argsIter));
break;
case SET:
putStringSet(editor, key, argsIter);
break;
}
editor.commit();
}
@Nonnull
private static String nextArg(Iterator<String> iter, String messageIfNotPresent)
throws DumpUsageException {
if (!iter.hasNext()) {
throw new DumpUsageException(messageIfNotPresent);
}
return iter.next();
}
@Nonnull
private static String nextArgValue(Iterator<String> iter) throws DumpUsageException {
return nextArg(iter, "Expected <value>");
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private static void putStringSet(
SharedPreferences.Editor editor,
String key,
Iterator<String> remainingArgs) {
HashSet<String> set = new HashSet<String>();
while (remainingArgs.hasNext()) {
set.add(remainingArgs.next());
}
editor.putStringSet(key, set);
}
/**
* Execute command to print all keys and values stored in the shared preferences which match
* the optional given prefix
*/
private void doPrint(PrintStream writer, List<String> args) {
String rootPath = mAppContext.getApplicationInfo().dataDir + "/shared_prefs";
String offsetPrefix = args.isEmpty() ? "" : args.get(0);
String keyPrefix = (args.size() > 1) ? args.get(1) : "";
printRecursive(writer, rootPath, "", offsetPrefix, keyPrefix);
}
private void printRecursive(
PrintStream writer,
String rootPath,
String offsetPath,
String pathPrefix,
String keyPrefix) {
File file = new File(rootPath, offsetPath);
if (file.isFile()) {
if (offsetPath.endsWith(XML_SUFFIX)) {
int suffixLength = XML_SUFFIX.length();
String prefsName = offsetPath.substring(0, offsetPath.length() - suffixLength);
printFile(writer, prefsName, keyPrefix);
}
} else if (file.isDirectory()) {
String[] children = file.list();
if (children != null) {
for (int i = 0; i < children.length; i++) {
String childOffsetPath = TextUtils.isEmpty(offsetPath)
? children[i]
: (offsetPath + File.separator + children[i]);
if (childOffsetPath.startsWith(pathPrefix)) {
printRecursive(writer, rootPath, childOffsetPath, pathPrefix, keyPrefix);
}
}
}
}
}
private void printFile(PrintStream writer, String prefsName, String keyPrefix) {
writer.println(prefsName + ":");
SharedPreferences preferences = getSharedPreferences(prefsName);
for (Map.Entry<String, ?> entry : preferences.getAll().entrySet()) {
if (entry.getKey().startsWith(keyPrefix)) {
writer.println(" " + entry.getKey() + " = " + entry.getValue());
}
}
}
private void doUsage(PrintStream writer) {
final String cmdName = "dumpapp " + NAME;
String usagePrefix = "Usage: " + cmdName + " ";
String blankPrefix = " " + cmdName + " ";
writer.println(usagePrefix + "<command> [command-options]");
writer.println(usagePrefix + "print [pathPrefix [keyPrefix]]");
writer.println(
Type.appendNamesList(
new StringBuilder(blankPrefix).append("write <path> <key> <"),
"|")
.append("> <value>"));
writer.println();
writer.println(cmdName + " print: Print all matching values from the shared preferences");
writer.println();
writer.println(cmdName + " write: Writes a value to the shared preferences");
}
private SharedPreferences getSharedPreferences(String name) {
return mAppContext.getSharedPreferences(name, Context.MODE_MULTI_PROCESS);
}
private enum Type {
BOOLEAN("boolean"),
INT("int"),
LONG("long"),
FLOAT("float"),
STRING("string"),
SET("set");
private final String name;
private Type(String name) {
this.name = name;
}
public static @Nullable Type of(String name) {
for (Type type : values()) {
if (type.name.equals(name)) {
return type;
}
}
return null;
}
public static StringBuilder appendNamesList(StringBuilder builder, String separator) {
boolean isFirst = true;
for (Type type : values()) {
if (isFirst) {
isFirst = false;
} else {
builder.append(separator);
}
builder.append(type.name);
}
return builder;
}
}
}