/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.constraintlayout.helper.widget;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import static androidx.constraintlayout.widget.ConstraintSet.Layout.UNSET_GONE_MARGIN;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Environment;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.RequiresApi;
import androidx.constraintlayout.motion.widget.Debug;
import androidx.constraintlayout.widget.ConstraintAttribute;
import androidx.constraintlayout.widget.ConstraintHelper;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
import androidx.constraintlayout.widget.R;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.HashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* This is a class is a debugging/logging utility to write out the constraints in JSON
* This is used for debugging purposes
* <ul>
* <li>logJsonTo - defines the output log console or "fileName"</li>
* <li>logJsonMode - mode one of:
* <b>periodic</b>, <b>delayed</b>, <b>layout</b> or <b>api</b></li>
* <li>logJsonDelay - the duration of the delay or the delay between repeated logs</li>
* </ul>
* logJsonTo supports:
* <ul>
* <li>log - logs using log.v("JSON5", ...)</li>
* <li>console - logs using System.out.println(...)</li>
* <li>[fileName] - will write to /storage/emulated/0/Download/[fileName].json5</li>
* </ul>
* logJsonMode modes are:
* <ul>
* <li>periodic - after window is attached will log every delay ms</li>
* <li>delayed - log once after delay ms</li>
* <li>layout - log every time there is a layout call</li>
* <li>api - do not automatically log developer will call writeLog</li>
* </ul>
*
* The defaults are:
* <ul>
* <li>logJsonTo="log"</li>
* <li>logJsonMode="delayed"</li>
* <li>logJsonDelay="1000"</li>
* </ul>
* Usage:
* <p></p>
* <pre>
* {@code
* <androidx.constraintlayout.helper.widget.LogJson
* android:layout_width="0dp"
* android:layout_height="0dp"
* android:visibility="gone"
* app:logJsonTo="log"
* app:logJsonMode="delayed"
* app:logJsonDelay="1000"
* />
* }
* </pre>
* </p>
*/
public class LogJson extends ConstraintHelper {
private static final String TAG = "JSON5";
private int mDelay = 1000;
private int mMode = LOG_DELAYED;
private String mLogToFile = null;
private boolean mLogConsole = true;
public static final int LOG_PERIODIC = 1;
public static final int LOG_DELAYED = 2;
public static final int LOG_LAYOUT = 3;
public static final int LOG_API = 4;
private boolean mPeriodic = false;
public LogJson(@androidx.annotation.NonNull Context context) {
super(context);
}
public LogJson(@androidx.annotation.NonNull Context context,
@androidx.annotation.Nullable AttributeSet attrs) {
super(context, attrs);
initLogJson(attrs);
}
public LogJson(@androidx.annotation.NonNull Context context,
@androidx.annotation.Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initLogJson(attrs);
}
private void initLogJson(AttributeSet attrs) {
if (attrs != null) {
TypedArray a = getContext().obtainStyledAttributes(attrs,
R.styleable.LogJson);
final int count = a.getIndexCount();
for (int i = 0; i < count; i++) {
int attr = a.getIndex(i);
if (attr == R.styleable.LogJson_logJsonDelay) {
mDelay = a.getInt(attr, mDelay);
} else if (attr == R.styleable.LogJson_logJsonMode) {
mMode = a.getInt(attr, mMode);
} else if (attr == R.styleable.LogJson_logJsonTo) {
TypedValue v = a.peekValue(attr);
if (v.type == TypedValue.TYPE_STRING) {
mLogToFile = a.getString(attr);
} else {
int value = a.getInt(attr, 0);
mLogConsole = value == 2;
}
}
}
a.recycle();
}
setVisibility(GONE);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
switch (mMode) {
case LOG_PERIODIC:
mPeriodic = true;
this.postDelayed(this::periodic, mDelay);
break;
case LOG_DELAYED:
this.postDelayed(this::writeLog, mDelay);
break;
case LOG_LAYOUT:
ConstraintLayout cl = (ConstraintLayout) getParent();
cl.addOnLayoutChangeListener((v, a, b, c, d, e, f, g, h) -> logOnLayout());
}
}
private void logOnLayout() {
if (mMode == LOG_LAYOUT) {
writeLog();
}
}
/**
* Set the duration of periodic logging of constraints
*
* @param duration the time in ms between writing files
*/
public void setDelay(int duration) {
mDelay = duration;
}
/**
* Start periodic sampling
*/
public void periodicStart() {
if (mPeriodic) {
return;
}
mPeriodic = true;
this.postDelayed(this::periodic, mDelay);
}
/**
* Stop periodic sampling
*/
public void periodicStop() {
mPeriodic = false;
}
private void periodic() {
if (mPeriodic) {
writeLog();
this.postDelayed(this::periodic, mDelay);
}
}
/**
* This writes a JSON5 representation of the constraintSet
*/
public void writeLog() {
String str = asString((ConstraintLayout) this.getParent());
if (mLogToFile == null) {
if (mLogConsole) {
System.out.println(str);
} else {
logBigString(str);
}
} else {
String name = toFile(str, mLogToFile);
Log.v("JSON", "\"" + name + "\" written!");
}
}
/**
* This writes the JSON5 description of the constraintLayout to a file named fileName.json5
* in the download directory which can be pulled with:
* "adb pull "/storage/emulated/0/Download/" ."
*
* @param str String to write as a file
* @param fileName file name
* @return full path name of file
*/
private static String toFile(String str, String fileName) {
FileOutputStream outputStream;
if (!fileName.endsWith(".json5")) {
fileName += ".json5";
}
try {
File down =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
File file = new File(down, fileName);
outputStream = new FileOutputStream(file);
outputStream.write(str.getBytes());
outputStream.close();
return file.getCanonicalPath();
} catch (IOException e) {
return e.toString();
}
}
@SuppressLint("LogConditional")
private void logBigString(String str) {
int len = str.length();
for (int i = 0; i < len; i++) {
int k = str.indexOf("\n", i);
if (k == -1) {
Log.v(TAG, str.substring(i));
break;
}
Log.v(TAG, str.substring(i, k));
i = k;
}
}
/**
* Get a JSON5 String that represents the Constraints in a running ConstraintLayout
*
* @param constraintLayout its constraints are converted to a string
* @return JSON5 string
*/
private static String asString(ConstraintLayout constraintLayout) {
JsonWriter c = new JsonWriter();
return c.constraintLayoutToJson(constraintLayout);
}
// ================================== JSON writer==============================================
private static class JsonWriter {
public static final int UNSET = ConstraintLayout.LayoutParams.UNSET;
ConstraintSet mSet;
Writer mWriter;
Context mContext;
int mUnknownCount = 0;
final String mLEFT = "left";
final String mRIGHT = "right";
final String mBASELINE = "baseline";
final String mBOTTOM = "bottom";
final String mTOP = "top";
final String mSTART = "start";
final String mEND = "end";
private static final String INDENT = " ";
private static final String SMALL_INDENT = " ";
HashMap<Integer, String> mIdMap = new HashMap<>();
private static final String LOG_JSON = LogJson.class.getSimpleName();
private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1);
HashMap<Integer, String> mNames = new HashMap<>();
private static int generateViewId() {
final int max_id = 0x00FFFFFF;
for (;;) {
final int result = sNextGeneratedId.get();
int newValue = result + 1;
if (newValue > max_id) {
newValue = 1;
}
if (sNextGeneratedId.compareAndSet(result, newValue)) {
return result;
}
}
}
@RequiresApi(17)
private static class JellyBean {
static int generateViewId() {
return View.generateViewId();
}
}
String constraintLayoutToJson(ConstraintLayout constraintLayout) {
StringWriter writer = new StringWriter();
int count = constraintLayout.getChildCount();
for (int i = 0; i < count; i++) {
View v = constraintLayout.getChildAt(i);
String name = v.getClass().getSimpleName();
int id = v.getId();
if (id == -1) {
if (android.os.Build.VERSION.SDK_INT
>= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
id = JellyBean.generateViewId();
} else {
id = generateViewId();
}
v.setId(id);
if (!LOG_JSON.equals(name)) {
name = "noid_" + name;
}
mNames.put(id, name);
} else if (LOG_JSON.equals(name)) {
mNames.put(id, name);
}
}
writer.append("{\n");
writeWidgets(writer, constraintLayout);
writer.append(" ConstraintSet:{\n");
ConstraintSet set = new ConstraintSet();
set.clone(constraintLayout);
String name =
(constraintLayout.getId() == -1) ? "cset" : Debug.getName(constraintLayout);
try {
writer.append(name + ":");
setup(writer, set, constraintLayout);
writeLayout();
writer.append("\n");
} catch (IOException e) {
throw new RuntimeException(e);
}
writer.append(" }\n");
writer.append("}\n");
return writer.toString();
}
private void writeWidgets(StringWriter writer, ConstraintLayout constraintLayout) {
writer.append("Widgets:{\n");
int count = constraintLayout.getChildCount();
for (int i = -1; i < count; i++) {
View v = (i == -1) ? constraintLayout : constraintLayout.getChildAt(i);
int id = v.getId();
if (LOG_JSON.equals(v.getClass().getSimpleName())) {
continue;
}
String name = mNames.containsKey(id) ? mNames.get(id)
: ((i == -1) ? "parent" : Debug.getName(v));
String cname = v.getClass().getSimpleName();
String bounds = ", bounds: [" + v.getLeft() + ", " + v.getTop()
+ ", " + v.getRight() + ", " + v.getBottom() + "]},\n";
writer.append(" " + name + ": { ");
if (i == -1) {
writer.append("type: '" + v.getClass().getSimpleName() + "' , ");
try {
ViewGroup.LayoutParams p = (ViewGroup.LayoutParams) v.getLayoutParams();
String wrap = "'WRAP_CONTENT'";
String match = "'MATCH_PARENT'";
String w = p.width == MATCH_PARENT ? match :
(p.width == WRAP_CONTENT) ? wrap : p.width + "";
writer.append("width: " + w + ", ");
String h = p.height == MATCH_PARENT ? match :
(p.height == WRAP_CONTENT) ? wrap : p.height + "";
writer.append("height: ").append(h);
} catch (Exception e) {
}
} else if (cname.contains("Text")) {
if (v instanceof TextView) {
writer.append("type: 'Text', label: '"
+ escape(((TextView) v).getText().toString()) + "'");
} else {
writer.append("type: 'Text' },\n");
}
} else if (cname.contains("Button")) {
if (v instanceof Button) {
writer.append("type: 'Button', label: '" + ((Button) v).getText() + "'");
} else
writer.append("type: 'Button'");
} else if (cname.contains("Image")) {
writer.append("type: 'Image'");
} else if (cname.contains("View")) {
writer.append("type: 'Box'");
} else {
writer.append("type: '" + v.getClass().getSimpleName() + "'");
}
writer.append(bounds);
}
writer.append("},\n");
}
private static String escape(String str) {
return str.replaceAll("'", "\'");
}
JsonWriter() {
}
void setup(Writer writer,
ConstraintSet set,
ConstraintLayout layout) throws IOException {
this.mWriter = writer;
this.mContext = layout.getContext();
this.mSet = set;
set.getConstraint(2);
}
private int[] getIDs() {
return mSet.getKnownIds();
}
private ConstraintSet.Constraint getConstraint(int id) {
return mSet.getConstraint(id);
}
private void writeLayout() throws IOException {
mWriter.write("{\n");
for (Integer id : getIDs()) {
ConstraintSet.Constraint c = getConstraint(id);
String idName = getSimpleName(id);
if (LOG_JSON.equals(idName)) { // skip LogJson it is for used to log
continue;
}
mWriter.write(SMALL_INDENT + idName + ":{\n");
ConstraintSet.Layout l = c.layout;
if (l.mReferenceIds != null) {
StringBuilder ref =
new StringBuilder("type: '_" + idName + "_' , contains: [");
for (int r = 0; r < l.mReferenceIds.length; r++) {
int rid = l.mReferenceIds[r];
ref.append((r == 0) ? "" : ", ").append(getName(rid));
}
mWriter.write(ref + "]\n");
}
if (l.mReferenceIdString != null) {
StringBuilder ref =
new StringBuilder(SMALL_INDENT + "type: '???' , contains: [");
String[] rids = l.mReferenceIdString.split(",");
for (int r = 0; r < rids.length; r++) {
String rid = rids[r];
ref.append((r == 0) ? "" : ", ").append("`").append(rid).append("`");
}
mWriter.write(ref + "]\n");
}
writeDimension("height", l.mHeight, l.heightDefault, l.heightPercent,
l.heightMin, l.heightMax, l.constrainedHeight);
writeDimension("width", l.mWidth, l.widthDefault, l.widthPercent,
l.widthMin, l.widthMax, l.constrainedWidth);
writeConstraint(mLEFT, l.leftToLeft, mLEFT, l.leftMargin, l.goneLeftMargin);
writeConstraint(mLEFT, l.leftToRight, mRIGHT, l.leftMargin, l.goneLeftMargin);
writeConstraint(mRIGHT, l.rightToLeft, mLEFT, l.rightMargin, l.goneRightMargin);
writeConstraint(mRIGHT, l.rightToRight, mRIGHT, l.rightMargin, l.goneRightMargin);
writeConstraint(mBASELINE, l.baselineToBaseline, mBASELINE, UNSET,
l.goneBaselineMargin);
writeConstraint(mBASELINE, l.baselineToTop, mTOP, UNSET, l.goneBaselineMargin);
writeConstraint(mBASELINE, l.baselineToBottom,
mBOTTOM, UNSET, l.goneBaselineMargin);
writeConstraint(mTOP, l.topToBottom, mBOTTOM, l.topMargin, l.goneTopMargin);
writeConstraint(mTOP, l.topToTop, mTOP, l.topMargin, l.goneTopMargin);
writeConstraint(mBOTTOM, l.bottomToBottom, mBOTTOM, l.bottomMargin,
l.goneBottomMargin);
writeConstraint(mBOTTOM, l.bottomToTop, mTOP, l.bottomMargin, l.goneBottomMargin);
writeConstraint(mSTART, l.startToStart, mSTART, l.startMargin, l.goneStartMargin);
writeConstraint(mSTART, l.startToEnd, mEND, l.startMargin, l.goneStartMargin);
writeConstraint(mEND, l.endToStart, mSTART, l.endMargin, l.goneEndMargin);
writeConstraint(mEND, l.endToEnd, mEND, l.endMargin, l.goneEndMargin);
writeVariable("horizontalBias", l.horizontalBias, 0.5f);
writeVariable("verticalBias", l.verticalBias, 0.5f);
writeCircle(l.circleConstraint, l.circleAngle, l.circleRadius);
writeGuideline(l.orientation, l.guideBegin, l.guideEnd, l.guidePercent);
writeVariable("dimensionRatio", l.dimensionRatio);
writeVariable("barrierMargin", l.mBarrierMargin);
writeVariable("type", l.mHelperType);
writeVariable("ReferenceId", l.mReferenceIdString);
writeVariable("mBarrierAllowsGoneWidgets",
l.mBarrierAllowsGoneWidgets, true);
writeVariable("WrapBehavior", l.mWrapBehavior);
writeVariable("verticalWeight", l.verticalWeight);
writeVariable("horizontalWeight", l.horizontalWeight);
writeVariable("horizontalChainStyle", l.horizontalChainStyle);
writeVariable("verticalChainStyle", l.verticalChainStyle);
writeVariable("barrierDirection", l.mBarrierDirection);
if (l.mReferenceIds != null) {
writeVariable("ReferenceIds", l.mReferenceIds);
}
writeTransform(c.transform);
writeCustom(c.mCustomConstraints);
mWriter.write(" },\n");
}
mWriter.write("},\n");
}
private void writeTransform(ConstraintSet.Transform transform) throws IOException {
if (transform.applyElevation) {
writeVariable("elevation", transform.elevation);
}
writeVariable("rotationX", transform.rotationX, 0);
writeVariable("rotationY", transform.rotationY, 0);
writeVariable("rotationZ", transform.rotation, 0);
writeVariable("scaleX", transform.scaleX, 1);
writeVariable("scaleY", transform.scaleY, 1);
writeVariable("translationX", transform.translationX, 0);
writeVariable("translationY", transform.translationY, 0);
writeVariable("translationZ", transform.translationZ, 0);
}
private void writeCustom(HashMap<String, ConstraintAttribute> cset) throws IOException {
if (cset != null && cset.size() > 0) {
mWriter.write(INDENT + "custom: {\n");
for (String s : cset.keySet()) {
ConstraintAttribute attr = cset.get(s);
if (attr == null) {
continue;
}
String custom = INDENT + SMALL_INDENT + attr.getName() + ": ";
switch (attr.getType()) {
case INT_TYPE:
custom += attr.getIntegerValue();
break;
case COLOR_TYPE:
custom += colorString(attr.getColorValue());
break;
case FLOAT_TYPE:
custom += attr.getFloatValue();
break;
case STRING_TYPE:
custom += "'" + attr.getStringValue() + "'";
break;
case DIMENSION_TYPE:
custom = custom + attr.getFloatValue();
break;
case REFERENCE_TYPE:
case COLOR_DRAWABLE_TYPE:
case BOOLEAN_TYPE:
custom = null;
}
if (custom != null) {
mWriter.write(custom + ",\n");
}
}
mWriter.write(SMALL_INDENT + " } \n");
}
}
private static String colorString(int v) {
String str = "00000000" + Integer.toHexString(v);
return "#" + str.substring(str.length() - 8);
}
private void writeGuideline(int orientation,
int guideBegin,
int guideEnd,
float guidePercent) throws IOException {
writeVariable("orientation", orientation);
writeVariable("guideBegin", guideBegin);
writeVariable("guideEnd", guideEnd);
writeVariable("guidePercent", guidePercent);
}
private void writeDimension(String dimString,
int dim,
int dimDefault,
float dimPercent,
int dimMin,
int dimMax,
boolean unusedConstrainedDim) throws IOException {
if (dim == 0) {
if (dimMax != UNSET || dimMin != UNSET) {
String s = "-----";
switch (dimDefault) {
case 0: // spread
s = INDENT + dimString + ": {value:'spread'";
break;
case 1: // wrap
s = INDENT + dimString + ": {value:'wrap'";
break;
case 2: // percent
s = INDENT + dimString + ": {value: '" + dimPercent + "%'";
break;
}
if (dimMax != UNSET) {
s += ", max: " + dimMax;
}
if (dimMax != UNSET) {
s += ", min: " + dimMin;
}
s += "},\n";
mWriter.write(s);
return;
}
switch (dimDefault) {
case 0: // spread is the default
break;
case 1: // wrap
mWriter.write(INDENT + dimString + ": '???????????',\n");
return;
case 2: // percent
mWriter.write(INDENT + dimString + ": '" + dimPercent + "%',\n");
}
} else if (dim == -2) {
mWriter.write(INDENT + dimString + ": 'wrap',\n");
} else if (dim == -1) {
mWriter.write(INDENT + dimString + ": 'parent',\n");
} else {
mWriter.write(INDENT + dimString + ": " + dim + ",\n");
}
}
private String getSimpleName(int id) {
if (mIdMap.containsKey(id)) {
return "" + mIdMap.get(id);
}
if (id == 0) {
return "parent";
}
String name = lookup(id);
mIdMap.put(id, name);
return "" + name + "";
}
private String getName(int id) {
return "'" + getSimpleName(id) + "'";
}
private String lookup(int id) {
try {
if (mNames.containsKey(id)) {
return mNames.get(id);
}
if (id != -1) {
return mContext.getResources().getResourceEntryName(id);
} else {
return "unknown" + ++mUnknownCount;
}
} catch (Exception ex) {
return "unknown" + ++mUnknownCount;
}
}
private void writeConstraint(String my,
int constraint,
String other,
int margin,
int goneMargin) throws IOException {
if (constraint == UNSET) {
return;
}
mWriter.write(INDENT + my);
mWriter.write(":[");
mWriter.write(getName(constraint));
mWriter.write(", ");
mWriter.write("'" + other + "'");
if (margin != 0 || goneMargin != UNSET_GONE_MARGIN) {
mWriter.write(", " + margin);
if (goneMargin != UNSET_GONE_MARGIN) {
mWriter.write(", " + goneMargin);
}
}
mWriter.write("],\n");
}
private void writeCircle(int circleConstraint,
float circleAngle,
int circleRadius) throws IOException {
if (circleConstraint == UNSET) {
return;
}
mWriter.write(INDENT + "circle");
mWriter.write(":[");
mWriter.write(getName(circleConstraint));
mWriter.write(", " + circleAngle);
mWriter.write(circleRadius + "],\n");
}
private void writeVariable(String name, int value) throws IOException {
if (value == 0 || value == -1) {
return;
}
mWriter.write(INDENT + name);
mWriter.write(": " + value);
mWriter.write(",\n");
}
private void writeVariable(String name, float value) throws IOException {
if (value == UNSET) {
return;
}
mWriter.write(INDENT + name);
mWriter.write(": " + value);
mWriter.write(",\n");
}
private void writeVariable(String name, float value, float def) throws IOException {
if (value == def) {
return;
}
mWriter.write(INDENT + name);
mWriter.write(": " + value);
mWriter.write(",\n");
}
private void writeVariable(String name, boolean value, boolean def) throws IOException {
if (value == def) {
return;
}
mWriter.write(INDENT + name);
mWriter.write(": " + value);
mWriter.write(",\n");
}
private void writeVariable(String name, int[] value) throws IOException {
if (value == null) {
return;
}
mWriter.write(INDENT + name);
mWriter.write(": ");
for (int i = 0; i < value.length; i++) {
mWriter.write(((i == 0) ? "[" : ", ") + getName(value[i]));
}
mWriter.write("],\n");
}
private void writeVariable(String name, String value) throws IOException {
if (value == null) {
return;
}
mWriter.write(INDENT + name);
mWriter.write(": '" + value);
mWriter.write("',\n");
}
}
}