/*
** Copyright 2012, Joel Pedraza
**
** 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 net.saik0.android.unifiedpreference;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import net.saik0.android.unifiedpreference.internal.utils.XmlUtils;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import com.actionbarsherlock.R;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Intent;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceActivity;
import android.preference.PreferenceCategory;
import android.preference.PreferenceActivity.Header;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.util.Xml;
@SuppressLint("NewApi")
public class UnifiedPreferenceHelper {
private final PreferenceActivity mActivity;
private int mHeaderRes;
private List<LegacyHeader> mLegacyHeaders = new LinkedList<LegacyHeader>();
private Boolean mSinglePane;
private String mSharedPreferencesName;
private int mSharedPreferencesMode;
public UnifiedPreferenceHelper(PreferenceActivity activity) {
mActivity = activity;
}
/**
* Default value for {@link Header#id Header.id} indicating that no
* identifier value is set. All other values (including those below -1) are
* valid.
*/
public static final long HEADER_ID_UNDEFINED = -1;
/**
* Determines whether the simplified settings UI should be shown. This is
* true if device doesn't have newer APIs like {@link PreferenceFragment},
* or if forced via {@link onIsHidingHeaders}, or the device doesn't have an
* extra-large screen. In these cases, a single-pane "simplified" settings
* UI should be shown.
*/
@SuppressLint("NewApi")
public boolean isSinglePane() {
if (mSinglePane == null) {
mSinglePane = Boolean
.valueOf(Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB
|| !mActivity.onIsMultiPane()
|| mActivity.onIsHidingHeaders());
}
return mSinglePane.booleanValue();
}
/**
* Returns the header resource to be used when building headers.
*
* @return The id of the header resource
*/
public int getHeaderRes() {
return mHeaderRes;
}
/**
* Sets the header resource to be used when building headers.
* This must be called before super.onCreate unless overriding both
* {@link #onBuildHeaders(List)} and {@link #onBuildLegacyHeaders(List)}
*
* @param headerRes The id of the header resource
*/
public void setHeaderRes(int headerRes) {
mHeaderRes = headerRes;
}
/**
* Returns the current name of the SharedPreferences file that preferences
* managed by this will use. Wraps
* {@link PreferenceManager#getSharedPreferencesName()} if single pane,
* otherwise returns a locally cached String.
*
* @return The name that can be passed to
* {@link Context#getSharedPreferences(String, int)}
*/
@SuppressWarnings("deprecation")
public String getSharedPreferencesName() {
if (mActivity.getPreferenceManager() != null) {
return mActivity.getPreferenceManager().getSharedPreferencesName();
}
return mSharedPreferencesName;
}
/**
* Sets the name of the SharedPreferences file that preferences managed by
* this will use. Wraps {@link PreferenceManager#setSharedPreferencesName()}
* if single pane, otherwise cache it for use by
* {@link UnifiedPreferenceFragment}.
*
* @param sharedPreferencesName The name of the SharedPreferences file.
*/
@SuppressWarnings("deprecation")
public void setSharedPreferencesName(String sharedPreferencesName) {
if (mActivity.getPreferenceManager() != null) {
mActivity.getPreferenceManager().setSharedPreferencesName(
sharedPreferencesName);
}
mSharedPreferencesName = sharedPreferencesName;
}
/**
* Returns the current mode of the SharedPreferences file that preferences
* managed by this will use. Wraps
* {@link PreferenceManager#getSharedPreferencesMode()} if single pane,
* otherwise returns a locally cached int.
*
* @return The mode that can be passed to
* {@link Context#getSharedPreferences(String, int)}
*/
@SuppressWarnings("deprecation")
public int getSharedPreferencesMode() {
if (mActivity.getPreferenceManager() != null) {
return mActivity.getPreferenceManager().getSharedPreferencesMode();
}
return mSharedPreferencesMode;
}
/**
* Sets the mode of the SharedPreferences file that preferences managed by
* this will use. Wraps {@link PreferenceManager#setSharedPreferencesMode()}
* if single pane, otherwise cache it for use by
* {@link UnifiedPreferenceFragment}.
*
* @param sharedPreferencesMode The mode of the SharedPreferences file.
*/
@SuppressWarnings("deprecation")
public void setSharedPreferencesMode(int sharedPreferencesMode) {
if (mActivity.getPreferenceManager() != null) {
mActivity.getPreferenceManager().setSharedPreferencesMode(
sharedPreferencesMode);
}
mSharedPreferencesMode = sharedPreferencesMode;
}
/**
* Shows the simplified settings UI if the device configuration if the
* device configuration dictates that a simplified, single-pane UI should be
* shown.
*/
@SuppressWarnings("deprecation")
protected void onPostCreate(Bundle savedInstanceState) {
if (isSinglePane()) {
// In the simplified UI, fragments are not used at all and we
// instead use the older PreferenceActivity APIs.
// Set PreferenceManager options if the activity received them before onPostCreate
if (!TextUtils.isEmpty(mSharedPreferencesName)) {
mActivity.getPreferenceManager().setSharedPreferencesName(mSharedPreferencesName);
}
if (mSharedPreferencesMode != 0) {
mActivity.getPreferenceManager().setSharedPreferencesMode(mSharedPreferencesMode);
}
onBuildLegacyHeaders(mLegacyHeaders);
// Add all preferences, first create a new preference screen if necessary
if (mActivity.getPreferenceScreen() == null) {
mActivity.setPreferenceScreen(mActivity.getPreferenceManager()
.createPreferenceScreen(mActivity));
}
for (LegacyHeader header : mLegacyHeaders) {
PreferenceCategory category = new PreferenceCategory(mActivity);
category.setTitle(header.getTitle(mActivity.getResources()));
mActivity.getPreferenceScreen().addPreference(category);
mActivity.addPreferencesFromResource(header.preferenceRes);
}
onBindPreferenceSummariesToValues();
}
}
/**
* Called when the activity needs its list of headers built. By implementing
* this and adding at least one item to the list, you will cause the
* activity to run in its modern fragment mode. Note that this function may
* not always be called; for example, if the activity has been asked to
* display a particular fragment without the header list, there is no need
* to build the headers.
*
* <p>
* Typical implementations will use {@link #loadHeadersFromResource} to fill
* in the list from a resource. For convenience this is done if a header
* resource has been set with {@link #setHeaderRes(int)}.
*
* @param target The list in which to place the headers.
*/
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public void onBuildHeaders(List<Header> target) {
// Do not build headers unless in single pane mode.
if (!isSinglePane() && mHeaderRes > 0) {
loadHeadersFromResource(mHeaderRes, target);
}
}
/**
* Called when the activity needs its list of legacy headers built.
*
* <p>
* Typical implementations will use {@link #loadLegacyHeadersFromResource}
* to fill in the list from a resource. For convenience this is done if a
* header resource has been set with {@link #setHeaderRes(int)}.
*
* @param target The list in which to place the legacy headers.
*/
public void onBuildLegacyHeaders(List<LegacyHeader> target) {
if (mHeaderRes > 0) {
loadLegacyHeadersFromResource(mHeaderRes, mLegacyHeaders);
}
}
/**
* Bind the summaries of EditText/List/Dialog/Ringtone preferences to their
* values. When their values change, their summaries are updated to reflect
* the new value, per the Android Design guidelines.
*/
@SuppressWarnings("deprecation")
public void onBindPreferenceSummariesToValues() {
UnifiedPreferenceUtils.bindAllPreferenceSummariesToValues(mActivity
.getPreferenceScreen());
}
/**
* Parse the given XML file as a header description, adding each parsed
* Header into the target list.
*
* @param resid The XML resource to load and parse.
* @param target The list in which the parsed headers should be placed.
*/
@SuppressLint("NewApi")
public void loadHeadersFromResource(int resid, List<Header> target) {
XmlResourceParser parser = null;
try {
parser = mActivity.getResources().getXml(resid);
AttributeSet attrs = Xml.asAttributeSet(parser);
int type;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& type != XmlPullParser.START_TAG) {
// Parse next until start tag is found
}
String nodeName = parser.getName();
if (!"preference-headers".equals(nodeName)) {
throw new RuntimeException(
"XML document must start with <preference-headers> tag; found"
+ nodeName + " at "
+ parser.getPositionDescription());
}
Bundle curBundle = null;
final int outerDepth = parser.getDepth();
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
continue;
}
nodeName = parser.getName();
if ("header".equals(nodeName)) {
Header header = new Header();
TypedArray sa = mActivity.getResources().obtainAttributes(
attrs, R.styleable.PreferenceHeader);
header.id = sa.getResourceId(
R.styleable.PreferenceHeader_id,
(int) HEADER_ID_UNDEFINED);
TypedValue tv = sa
.peekValue(R.styleable.PreferenceHeader_title);
if (tv != null && tv.type == TypedValue.TYPE_STRING) {
if (tv.resourceId != 0) {
header.titleRes = tv.resourceId;
} else {
header.title = tv.string;
}
}
tv = sa.peekValue(R.styleable.PreferenceHeader_summary);
if (tv != null && tv.type == TypedValue.TYPE_STRING) {
if (tv.resourceId != 0) {
header.summaryRes = tv.resourceId;
} else {
header.summary = tv.string;
}
}
tv = sa.peekValue(R.styleable.PreferenceHeader_breadCrumbTitle);
if (tv != null && tv.type == TypedValue.TYPE_STRING) {
if (tv.resourceId != 0) {
header.breadCrumbTitleRes = tv.resourceId;
} else {
header.breadCrumbTitle = tv.string;
}
}
tv = sa.peekValue(R.styleable.PreferenceHeader_breadCrumbShortTitle);
if (tv != null && tv.type == TypedValue.TYPE_STRING) {
if (tv.resourceId != 0) {
header.breadCrumbShortTitleRes = tv.resourceId;
} else {
header.breadCrumbShortTitle = tv.string;
}
}
header.iconRes = sa.getResourceId(
R.styleable.PreferenceHeader_icon, 0);
header.fragment = sa
.getString(R.styleable.PreferenceHeader_fragment);
if (curBundle == null) {
curBundle = new Bundle();
}
// Add preference resource to fragmentArgs
int preference = sa.getResourceId(
R.styleable.PreferenceHeader_preferenceRes, 0);
if (preference > 0) {
curBundle.putInt(
UnifiedPreferenceFragment.ARG_PREFERENCE_RES, preference);
}
sa.recycle();
final int innerDepth = parser.getDepth();
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser
.getDepth() > innerDepth)) {
if (type == XmlPullParser.END_TAG
|| type == XmlPullParser.TEXT) {
continue;
}
String innerNodeName = parser.getName();
if (innerNodeName.equals("extra")) {
mActivity.getResources().parseBundleExtra("extra",
attrs, curBundle);
XmlUtils.skipCurrentTag(parser);
} else if (innerNodeName.equals("intent")) {
header.intent = Intent.parseIntent(
mActivity.getResources(), parser, attrs);
} else {
XmlUtils.skipCurrentTag(parser);
}
}
if (curBundle.size() > 0) {
header.fragmentArguments = curBundle;
curBundle = null;
}
target.add(header);
} else {
XmlUtils.skipCurrentTag(parser);
}
}
} catch (XmlPullParserException e) {
throw new RuntimeException("Error parsing headers", e);
} catch (IOException e) {
throw new RuntimeException("Error parsing headers", e);
} finally {
if (parser != null)
parser.close();
}
}
/**
* Parse the given XML file as a header description, adding each parsed
* LegacyHeader into the target list.
*
* @param resid The XML resource to load and parse.
* @param target The list in which the parsed headers should be placed.
*/
public void loadLegacyHeadersFromResource(int resid, List<LegacyHeader> target) {
XmlResourceParser parser = null;
try {
parser = mActivity.getResources().getXml(resid);
AttributeSet attrs = Xml.asAttributeSet(parser);
int type;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& type != XmlPullParser.START_TAG) {
// Parse next until start tag is found
}
String nodeName = parser.getName();
if (!"preference-headers".equals(nodeName)) {
throw new RuntimeException(
"XML document must start with <preference-headers> tag; found"
+ nodeName + " at "
+ parser.getPositionDescription());
}
final int outerDepth = parser.getDepth();
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
continue;
}
nodeName = parser.getName();
if ("header".equals(nodeName)) {
LegacyHeader header = new LegacyHeader();
TypedArray sa = mActivity.getResources().obtainAttributes(
attrs, R.styleable.PreferenceHeader);
TypedValue tv = sa
.peekValue(R.styleable.PreferenceHeader_title);
if (tv != null && tv.type == TypedValue.TYPE_STRING) {
if (tv.resourceId != 0) {
header.titleRes = tv.resourceId;
} else {
header.title = tv.string;
}
}
header.preferenceRes = sa.getResourceId(
R.styleable.PreferenceHeader_preferenceRes, 0);
sa.recycle();
target.add(header);
} else {
XmlUtils.skipCurrentTag(parser);
}
}
} catch (XmlPullParserException e) {
throw new RuntimeException("Error parsing headers", e);
} catch (IOException e) {
throw new RuntimeException("Error parsing headers", e);
} finally {
if (parser != null)
parser.close();
}
}
}