/* * Copyright (C) 2006 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. * * stolen almost completely from ArrayAdapter.java - nolan */ package com.nolanlawson.logcat.data; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; import android.content.Context; import android.text.TextPaint; import android.text.TextUtils; import android.text.TextUtils.TruncateAt; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.Filter; import android.widget.Filterable; import android.widget.TextView; import com.nolanlawson.logcat.R; import com.nolanlawson.logcat.helper.PreferenceHelper; import com.nolanlawson.logcat.util.LogLineAdapterUtil; import com.nolanlawson.logcat.util.StopWatch; import com.nolanlawson.logcat.util.UtilLogger; /** * A ListAdapter that manages a ListView backed by an array of arbitrary * objects. By default this class expects that the provided resource id references * a single TextView. If you want to use a more complex layout, use the constructors that * also takes a field id. That field id should reference a TextView in the larger layout * resource. * * However the TextView is referenced, it will be filled with the toString() of each object in * the array. You can add lists or arrays of custom objects. Override the toString() method * of your objects to determine what text will be displayed for the item in the list. * * To use something other than TextViews for the array display, for instance, ImageViews, * or to have some of data besides toString() results fill the views, * override {@link #getView(int, View, ViewGroup)} to return the type of view you want. */ public class LogLineAdapter extends BaseAdapter implements Filterable { private static UtilLogger log = new UtilLogger(LogLineAdapter.class); private Comparator<? super LogLine> mComparator; /** * Contains the list of objects that represent the data of this ArrayAdapter. * The content of this list is referred to as "the array" in the documentation. */ private List<LogLine> mObjects; /** * Lock used to modify the content of {@link #mObjects}. Any write operation * performed on the array should be synchronized on this lock. This lock is also * used by the filter (see {@link #getFilter()} to make a synchronized copy of * the original array of data. */ private final Object mLock = new Object(); /** * The resource indicating what views to inflate to display the content of this * array adapter. */ private int mResource; /** * The resource indicating what views to inflate to display the content of this * array adapter in a drop down widget. */ private int mDropDownResource; /** * Indicates whether or not {@link #notifyDataSetChanged()} must be called whenever * {@link #mObjects} is modified. */ private boolean mNotifyOnChange = true; private Context mContext; private ArrayList<LogLine> mOriginalValues; private ArrayFilter mFilter; private LayoutInflater mInflater; private int logLevelLimit = 0; /** * Constructor * * @param context The current context. * @param textViewResourceId The resource ID for a layout file containing a TextView to use when * instantiating views. * @param objects The objects to represent in the ListView. */ public LogLineAdapter(Context context, int textViewResourceId, List<LogLine> objects) { init(context, textViewResourceId, 0, objects); } /** * Adds the specified object at the end of the array. * * @param object The object to add at the end of the array. */ public void add(LogLine object) { if (mOriginalValues != null) { synchronized (mLock) { mOriginalValues.add(object); mObjects.add(object); if (mNotifyOnChange) notifyDataSetChanged(); } } else { mObjects.add(object); if (mNotifyOnChange) notifyDataSetChanged(); } } public void addWithFilter(LogLine object, CharSequence text) { if (mOriginalValues != null) { List<LogLine> inputList = Arrays.asList(object); if (mFilter == null) { mFilter = new ArrayFilter(); } List<LogLine> filteredObjects = mFilter.performFilteringOnList(inputList, text); synchronized (mLock) { mOriginalValues.add(object); mObjects.addAll(filteredObjects); if (mNotifyOnChange) notifyDataSetChanged(); } } else { synchronized (mLock) { mObjects.add(object); } if (mNotifyOnChange) notifyDataSetChanged(); } } /** * Inserts the specified object at the specified index in the array. * * @param object The object to insert into the array. * @param index The index at which the object must be inserted. */ public void insert(LogLine object, int index) { if (mOriginalValues != null) { synchronized (mLock) { mOriginalValues.add(index, object); if (mNotifyOnChange) notifyDataSetChanged(); } } else { mObjects.add(index, object); if (mNotifyOnChange) notifyDataSetChanged(); } } /** * Removes the specified object from the array. * * @param object The object to remove. */ public void remove(LogLine object) { if (mOriginalValues != null) { synchronized (mLock) { mOriginalValues.remove(object); } } else { mObjects.remove(object); } if (mNotifyOnChange) notifyDataSetChanged(); } public void removeFirst(int n) { StopWatch stopWatch = new StopWatch("removeFirst()"); if (mOriginalValues != null) { synchronized (mLock) { List<LogLine> subList = mOriginalValues.subList(n, mOriginalValues.size()); for (int i = 0; i < n; i++) { // value to delete - delete it from the mObjects as well mObjects.remove(mOriginalValues.get(i)); } mOriginalValues = new ArrayList<LogLine>(subList); } } else { synchronized (mLock) { mObjects = new ArrayList<LogLine>(mObjects.subList(n, mObjects.size())); } } if (mNotifyOnChange) notifyDataSetChanged(); stopWatch.log(log); } /** * Remove all elements from the list. */ public void clear() { if (mOriginalValues != null) { synchronized (mLock) { mOriginalValues.clear(); mObjects.clear(); } } else { mObjects.clear(); } if (mNotifyOnChange) notifyDataSetChanged(); } /** * Sorts the content of this adapter using the specified comparator. * * @param comparator The comparator used to sort the objects contained * in this adapter. */ public void sort(Comparator<? super LogLine> comparator) { this.mComparator = comparator; Collections.sort(mObjects, comparator); if (mNotifyOnChange) notifyDataSetChanged(); } /** * {@inheritDoc} */ @Override public void notifyDataSetChanged() { super.notifyDataSetChanged(); mNotifyOnChange = true; } /** * Control whether methods that change the list ({@link #add}, * {@link #insert}, {@link #remove}, {@link #clear}) automatically call * {@link #notifyDataSetChanged}. If set to false, caller must * manually call notifyDataSetChanged() to have the changes * reflected in the attached view. * * The default is true, and calling notifyDataSetChanged() * resets the flag to true. * * @param notifyOnChange if true, modifications to the list will * automatically call {@link * #notifyDataSetChanged} */ public void setNotifyOnChange(boolean notifyOnChange) { mNotifyOnChange = notifyOnChange; } private void init(Context context, int resource, int textViewResourceId, List<LogLine> objects) { mContext = context; mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mResource = mDropDownResource = resource; mObjects = objects; } /** * Returns the context associated with this array adapter. The context is used * to create views from the resource passed to the constructor. * * @return The Context associated with this adapter. */ public Context getContext() { return mContext; } /** * {@inheritDoc} */ public int getCount() { synchronized (mLock) { return mObjects.size(); } } /** * {@inheritDoc} */ public LogLine getItem(int position) { return mObjects.get(position); } public List<LogLine> getTrueValues() { return mOriginalValues != null ? mOriginalValues : mObjects; } /** * Returns the position of the specified item in the array. * * @param item The item to retrieve the position of. * * @return The position of the specified item. */ public int getPosition(LogLine item) { return mObjects.indexOf(item); } /** * {@inheritDoc} */ public long getItemId(int position) { return position; } public View getView(int position, View convertView, ViewGroup parent) { return createViewFromResource(position, convertView, parent, mResource); } private View createViewFromResource(int position, View view, ViewGroup parent, int resource) { Context context = parent.getContext(); LogLineViewWrapper wrapper; if (view == null) { view = mInflater.inflate(R.layout.logcat_list_item, parent, false); wrapper = new LogLineViewWrapper(view); view.setTag(wrapper); } else { wrapper = (LogLineViewWrapper) view.getTag(); } TextView levelTextView = wrapper.getLevelTextView(); TextView outputTextView = wrapper.getOutputTextView(); TextView tagTextView = wrapper.getTagTextView(); TextView pidTextView = wrapper.getPidTextView(); TextView timestampTextView = wrapper.getTimestampTextView(); LogLine logLine; try { logLine = getItem(position); } catch (IndexOutOfBoundsException ignore) { // XXX hack - I sometimes get array index out of bounds exceptions here // no idea how to solve it, so this is the best I can do logLine = LogLine.newLogLine("", PreferenceHelper.getExpandedByDefaultPreference(context)); } levelTextView.setText(Character.toString(LogLine.convertLogLevelToChar(logLine.getLogLevel()))); levelTextView.setBackgroundColor(LogLineAdapterUtil.getBackgroundColorForLogLevel(context, logLine.getLogLevel())); levelTextView.setTextColor(LogLineAdapterUtil.getForegroundColorForLogLevel(context, logLine.getLogLevel())); levelTextView.setVisibility(logLine.getLogLevel() == -1 ? View.GONE : View.VISIBLE); int foregroundColor = PreferenceHelper.getColorScheme(context).getForegroundColor(context); CharSequence output = (logLine.isExpanded() || TextUtils.isEmpty(logLine.getTag())) // empty tag indicates this is the line like "beginning of dev/log..." ? logLine.getLogOutput() : ellipsizeString(logLine.getLogOutput(), outputTextView); outputTextView.setSingleLine(!logLine.isExpanded()); outputTextView.setText(output); outputTextView.setTextColor(foregroundColor); CharSequence tag = logLine.isExpanded() ? logLine.getTag() : ellipsizeString(logLine.getTag(), tagTextView); tagTextView.setSingleLine(!logLine.isExpanded()); tagTextView.setText(tag); tagTextView.setVisibility(logLine.getLogLevel() == -1 ? View.GONE : View.VISIBLE); // set the text size based on the preferences float textSize = PreferenceHelper.getTextSizePreference(context); tagTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize); outputTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize); levelTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize); boolean extraInfoIsVisible = logLine.isExpanded() && PreferenceHelper.getShowTimestampAndPidPreference(context) && logLine.getProcessId() != -1; // -1 marks lines like 'beginning of /dev/log...' pidTextView.setVisibility(extraInfoIsVisible ? View.VISIBLE : View.GONE); timestampTextView.setVisibility(extraInfoIsVisible ? View.VISIBLE : View.GONE); if (extraInfoIsVisible) { pidTextView.setTextColor(foregroundColor); timestampTextView.setTextColor(foregroundColor); pidTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize); timestampTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize); pidTextView.setText(logLine.getProcessId() != -1 ? Integer.toString(logLine.getProcessId()) : null); timestampTextView.setText(logLine.getTimestamp()); } tagTextView.setTextColor(LogLineAdapterUtil.getOrCreateTagColor(context, logLine.getTag())); // if this is a "partially selected" log, change the color to orange or whatever int selectedBackground = logLine.isHighlighted() ? PreferenceHelper.getColorScheme(context).getSelectedColor(context) : context.getResources().getColor(android.R.color.transparent); view.setBackgroundColor(selectedBackground); return view; } /** * Optimization for when you have TextViews that truncate, per * http://www.martinadamek.com/2011/01/05/performance-of-android-listview-containing-textviews/ * @param str * @param textView * @return */ private CharSequence ellipsizeString(CharSequence str, TextView textView) { int width = textView.getWidth() - textView.getCompoundPaddingLeft() - textView.getCompoundPaddingRight(); return ellipsizeString(str, width, textView.getPaint()); } private CharSequence ellipsizeString(CharSequence str, int width, TextPaint textPaint) { if (TextUtils.isEmpty(str)) { return str; } if (width <= 0) { // view probably hasn't been drawn yet; just return the whole string to avoid blank textviews return str; } return ellipsizeFromCache(str, width, textPaint); } private CharSequence ellipsizeFromCache(CharSequence str, int width, TextPaint paint) { // the TextUtils.ellipsize method is really expensive, so we can exploit the fact that we're using monospace-style text // to just cache the correct number of characters given the width Integer maxLength = PreferenceHelper.ellipsisLengthsCache.get(width); if (maxLength != null) { // cached if (str.length() < maxLength) { return str; } // truncate and add ellipsis StringBuilder stringBuilder = new StringBuilder(str); if (stringBuilder.length() > maxLength) { stringBuilder.delete(maxLength, stringBuilder.length()); } if (stringBuilder.length() > 0) { stringBuilder.setCharAt(stringBuilder.length() - 1, (char) 8230); // add ellipsis character } return stringBuilder; } CharSequence result = TextUtils.ellipsize(str, paint, width, TruncateAt.END); if (result.length() < str.length()) { // was ellipsized PreferenceHelper.ellipsisLengthsCache.put(width, result.length()); } return result; } /** * <p>Sets the layout resource to create the drop down views.</p> * * @param resource the layout resource defining the drop down views * @see #getDropDownView(int, android.view.View, android.view.ViewGroup) */ public void setDropDownViewResource(int resource) { this.mDropDownResource = resource; } /** * {@inheritDoc} */ @Override public View getDropDownView(int position, View convertView, ViewGroup parent) { return createViewFromResource(position, convertView, parent, mDropDownResource); } public int getLogLevelLimit() { return logLevelLimit; } public void setLogLevelLimit(int logLevelLimit) { this.logLevelLimit = logLevelLimit; } /** * {@inheritDoc} */ public Filter getFilter() { if (mFilter == null) { mFilter = new ArrayFilter(); } return mFilter; } public List<LogLine> getObjects() { return mObjects; } /** * <p>An array filter constrains the content of the array adapter with * a prefix. Each item that does not start with the supplied prefix * is removed from the list.</p> */ private class ArrayFilter extends Filter { @Override protected FilterResults performFiltering(CharSequence prefix) { FilterResults results = new FilterResults(); if (mOriginalValues == null) { synchronized (mLock) { mOriginalValues = new ArrayList<LogLine>(mObjects); } } ArrayList<LogLine> allValues = performFilteringOnList(mOriginalValues, prefix); results.values = allValues; results.count = allValues.size(); return results; } public ArrayList<LogLine> performFilteringOnList(List<LogLine> inputList, CharSequence query) { SearchCriteria searchCriteria = new SearchCriteria(query); // search by log level ArrayList<LogLine> allValues = new ArrayList<LogLine>(); ArrayList<LogLine> logLines; synchronized (mLock) { logLines = new ArrayList<LogLine>(inputList); } for (LogLine logLine : logLines) { if (logLine != null && LogLineAdapterUtil.logLevelIsAcceptableGivenLogLevelLimit(logLine.getLogLevel(), logLevelLimit)) { allValues.add(logLine); } } ArrayList<LogLine> finalValues = allValues; // search by criteria if (!searchCriteria.isEmpty()) { final ArrayList<LogLine> values = allValues; final int count = values.size(); final ArrayList<LogLine> newValues = new ArrayList<LogLine>(count); for (int i = 0; i < count; i++) { final LogLine value = values.get(i); // search the logline based on the criteria if (searchCriteria.matches(value)) { newValues.add(value); } } finalValues = newValues; } // sort here to ensure that filtering the list doesn't mess up the sorting if (mComparator != null) { Collections.sort((List<LogLine>)finalValues, mComparator); } return finalValues; } @SuppressWarnings("unchecked") @Override protected void publishResults(CharSequence constraint, FilterResults results) { //noinspection unchecked //log.d("filtering: %s", constraint); mObjects = (List<LogLine>) results.values; if (results.count > 0) { notifyDataSetChanged(); } else { notifyDataSetInvalidated(); } } } }