isValidFragment Android API 19

49
当我在Android KitKat上尝试运行我的应用程序时,PreferenceActivity中出现了一个错误。 子类PreferenceActivity必须重写isValidFragment(String)以验证Fragment类是否有效!com.crbin1.labeltodo.ActivityPreference未检查片段com.crbin1.labeltodo.StockPreferenceFragment是否有效。 在文档中,我找到了以下解释: 受保护的布尔值isValidFragment(String fragmentName)。 添加于API级别19. 子类应该重写此方法并验证给定的片段是否是附加到此活动的有效类型。对于android:targetSdkVersion旧版本的应用程序,默认实现返回true。对于后续版本,它将引发异常。 我找不到任何解决问题的示例。

你尝试过覆盖它吗? - Blackbelt
如果我用简单的“返回true”覆盖该方法,它可以工作,但现在的问题是:“我必须在这个重写的方法中执行什么检查?” - crbin1
1
我认为第一个问题应该是:“什么是有效的片段?” - Hitman
我同意,什么是有效的片段? :-) - crbin1
3
现在我的所有应用程序都在4.4上抛出了这个异常。什么鬼? - Divers
10个回答

65

试一下这个...这就是我们检查片段有效性的方法。

protected boolean isValidFragment(String fragmentName) {
  return StockPreferenceFragment.class.getName().equals(fragmentName);
}

4
需要为每个片段都这样做吗?或者只需要偏好设置呢? - Phantômaxx
1
@Wolkenjaeger 你可以将你的碎片名称存储在一个数组中,并从那里读取。API会比较名称,所以你必须找到一种方法让你的代码匹配字符串。使用标准String操作来抽象化它不应该很难。毕竟,你要负责向系统返回一个简单的布尔值。创建自己的代码逻辑取决于你。 - davidcesarino
2
所以在PreferenceActivity中,我重写了这个方法并返回true... 这似乎是一件相当愚蠢的事情,是吧? - Phantômaxx
2
@Tobor 为什么这样做很愚蠢?这是一个简单的安全检查,以确保在调用片段时,该片段是经过授权的(即您认可的那些片段)。如果您没有任何逻辑地只是return true,那么这就毫无意义了。这就好像白宫授权每个走过公共门口的人,但只有那些在那里工作的人才能正确通过return true - davidcesarino
2
非常糟糕的是,当你通过Android Studio添加PreferenceActivity时,你得到的默认实现并没有解决这个问题。 - Roel
显示剩余3条评论

24

出于纯粹的好奇心,你也可以这样做:

@Override
protected boolean isValidFragment(String fragmentName) {
    return MyPreferenceFragmentA.class.getName().equals(fragmentName)
            || MyPreferenceFragmentB.class.getName().equals(fragmentName)
            || // ... Finish with your last fragment.

;}

20

我发现可以在我的头部资源加载时从中获取我的片段名称的副本:

public class MyActivity extends PreferenceActivity
{
    private static List<String> fragments = new ArrayList<String>();

    @Override
    public void onBuildHeaders(List<Header> target)
    {
        loadHeadersFromResource(R.xml.headers,target);
        fragments.clear();
        for (Header header : target) {
            fragments.add(header.fragment);
        }
    }
...
    @Override
    protected boolean isValidFragment(String fragmentName)
    {
        return fragments.contains(fragmentName);
    }
}

这样我就不需要记住在代码中埋藏的片段列表,如果我想要更新它们,也不需要手动更新。

我曾希望直接使用getHeaders()和现有的标题列表,但似乎在onBuildHeaders()之后活动被销毁,在调用isValidFragment()之前重新创建。

这可能是因为我测试的Nexus 7实际上并不支持双窗格首选项活动。因此也需要静态列表成员。


太棒了!我在口味上遇到了问题,因为在开发、测试和生产环境下都有不同的碎片加载。这个解决方案就像梦想一样奏效了。 - Nabdreas
不错的解决方案!运行得很好! - void pointer
更干净、更好的解决方案 :) - Al-Mothafar
我没有看到Ofek Ron在他重写这个最佳解决方案时提到的安全漏洞。 - John
这是个坏主意!!!特别是当你启动PreferenceActivity并设置extras时 -> Intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMEN,Fragment.class.getName()); Intent.putExtra(PreferenceActivity.EXTRA_NO_HEADERS, true); 然后你就会陷入黑洞 :) - ceph3us

18

由于新发现的漏洞,该API被添加。请参见http://ibm.co/1bAA8kFhttp://ibm.co/IDm2Es

2013年12月10日 "我们最近向Android安全团队披露了一个新漏洞[...]更准确地说,任何使用导出活动扩展PreferenceActivity类的应用程序都自动处于易受攻击的状态。Android KitKat中提供了补丁。如果你想知道为什么你的代码现在无法工作,那是由于Android KitKat补丁需要应用程序重写新方法PreferenceActivity.isValidFragment,该方法已添加到Android Framework中。"--来自上面第一个链接


8
你好,@RoeeHay,请你对所链接的文章进行详细阐述,因为仅仅提供一个链接作为回答通常被认为是不好的回答方式。能否请你指出漏洞中的重要点?我看到这篇文章是由你撰写的,它解释了很多关于为什么引入该漏洞的内容。我很想给这个回答点赞,因为它远远地解释了为什么而不是如何修复这个问题。 - Avinash R

3

我不确定Lane的实现是否没有被讨论这里中提到的漏洞,但如果是的话,我认为更好的解决方案是避免使用静态列表,只需执行以下操作:

 @Override
    protected boolean isValidFragment(String fragmentName)
    {
        ArrayList<Header> target = new ArrayList<>();
        loadHeadersFromResource(R.xml.pref_headers, target);
        for (Header h : target) {
            if (fragmentName.equals(h.fragment)) return true;
        }
        return false;
    }

正如我对@lane的回答所说 - 使用onBuildHeaders(List <>)方法设置片段列表是一个不好的主意 - 您的方法更为安全 - 我投票支持您!但您可以更进一步改进它 - 创建一个私有目标,如果为空则填充它。 - ceph3us

3

已通过实际4.4设备验证:

(1) 如果您的proguard.cfg文件有这行代码(很多人都定义了):

-keep public class com.fullpackage.MyPreferenceFragment

(2)最有效的实现方式是:
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public class EditPreferencesHC extends PreferenceActivity {
...
   protected boolean isValidFragment (String fragmentName) {

     return "com.fullpackage.MyPreferenceFragment".equals(fragmentName);

   }
}

0

我的解决方案(而不是创建类的ArrayList),因为加载的片段应该是PreferenceFragment.class的子类,所以在@OverRide方法中运行此检查

@Override
protected boolean isValidFragment(String fragmentName) {
    try {
        Class cls = Class.forName(fragmentName);
        return (cls.getSuperclass().equals(PreferenceFragment.class));
                                  // true if superclass is PreferenceFragmnet
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
    return false;
}

0

这是我的解决方案:

  • 如果您需要动态重建标题
  • 如果您使用额外的方式启动首选项活动 - onBuildHeaders() 方法将失败!(使用下面的启动意图额外参数 - 为什么? - 简单,因为 onBuildHeaders() 从未被调用):

    Intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMEN,Fragment.class.getName()); Intent.putExtra(PreferenceActivity.EXTRA_NO_HEADERS, true);

这是示例类:

/**
 * Preference Header for showing settings and add view as two panels for tablets
 * for ActionBar we need override onCreate and setContentView
 */
public class SettingsPreferenceActivity extends PreferenceActivity {

    /** valid fragment list declaration */
    private List<String> validFragmentList;

    /** some example irrelevant class for holding user session  */
    SessionManager _sessionManager;

    @Override
    public void onBuildHeaders(List<Header> target) {
        /** load header from res */
        loadHeadersFromResource(getValidResId(), target);
    }

    /**
     * this API method was added due to a newly discovered vulnerability.
     */
    @Override
    protected boolean isValidFragment(String fragmentName) {
        List<Header> headers = new ArrayList<>();
        /** fill fragments list */
        tryObtainValidFragmentList(getValidResId(), headers);
        /** check  id valid */
        return validFragmentList.contains(fragmentName);
    }

    /** try fill list of valid fragments */
    private void tryObtainValidFragmentList(int resourceId, List<Header> target) {  
        /** check for null */
        if(validFragmentList==null) {
            /** init */
            validFragmentList = new ArrayList();
        } else {
            /** clear */
            validFragmentList.clear();
        }
        /** load headers to list */
        loadHeadersFromResource(resourceId, target);
        /** set headers class names to list */
        for (Header header : target) {
            /** fill */
            validFragmentList.add(header.fragment);
        }
    }

    /** obtain valid res id to build headers */
    private int getValidResId() {
        /** get session manager */
        _sessionManager = SessionManager.getInstance();
        /** check if user is authorized */
        if (_sessionManager.getCurrentUser().getWebPart().isAuthorized()) {
            /** if is return full preferences header */
            return R.xml.settings_preferences_header_logged_in;
        } else {
            /** else return short header */
            return R.xml.settings_preferences_header_logged_out;
        }
    }
}

0
这是我的headers_preferences.xml文件:
<?xml version="1.0" encoding="utf-8"?>  
<preference-headers  
xmlns:android="http://schemas.android.com/apk/res/android">  

    <header  

        android:fragment="com.gammazero.signalrocket.AppPreferencesFragment$Prefs1Fragment"  
        android:title="Change Your Name" />  

    <header  
        android:fragment="com.gammazero.signalrocket.AppPreferencesFragment$Prefs2Fragment"  
        android:title="Change Your Group''s Name" />  

    <header  
        android:fragment="com.gammazero.signalrocket.AppPreferencesFragment$Prefs3Fragment"  
        android:title="Change Map View" />  

</preference-headers>  

在我的PreferencesActivity中,isValidFragment代码出现了,我只是将其反过来了:
@Override
protected boolean isValidFragment(String fragmentName)
{
  //  return AppPreferencesFragment.class.getName().contains(fragmentName);
    return fragmentName.contains (AppPreferencesFragment.class.getName());
}

只要我在所有片段名称的开头使用AppPreferencesFragment字符串,它们都可以很好地验证。

0
@Override
protected boolean isValidFragment (String fragmentName) {
    for (Class<?> cls : ImePreferences.class.getDeclaredClasses()) {
        if (cls.getName().equals(fragmentName)){return true;}
    }
    return false;
}

2
你好,感谢您的首次贡献!您能否添加更多细节(为什么应该使用这个代码片段而不是其他答案之一)? - Carsten Hagemann

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接