Android 应用间资源覆盖

该方案基于android 9.x验证

问题描述

客户想要我们的主应用MainApp,但是想通过值改变资源应用ResApp,达到改变应用多语言翻译的需求

也就是,我们提供给客户MainApp的apk文件,和ResApp的源码文件,客户通过改变ResApp,来控制MainApp的显示。

解决方案 1

  1. 将两个apk shared到同一个进程中去
//主应用 MainApp
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.jdf.res.main"
    android:sharedUserId="com.jdf.res">

//资源应用 ResApp
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.jdf.res.resapp"
    android:sharedUserId="com.jdf.res">
  1. 将两个应用新增同样名称,不同值的资源字段
//主应用 MainApp-默认资源
<resources>
    <string name="app_name">MainApp</string>
    <string name="test1">Main App test1</string>
    <string name="test2">Main App test2</string>
    <string name="test3">Main App test3</string>
    <string name="test4">Main App test4</string>
    <string name="test5">Main App test5</string>
    <string name="test6">Main App test6</string>

</resources>

//资源应用 ResApp--默认资源
<resources>
    <string name="app_name">ResApp</string>
    <string name="test1">Res app test1</string>
    <string name="test2">Res app test2</string>
    <string name="test3">Res app test3</string>
    <string name="test4">Res app test4</string>
    <string name="test5">Res app test5</string>
    <string name="test6">Res app test6</string>
</resources>

//资源应用 ResApp--中文资源
<resources>
    <string name="app_name">资源应用</string>
    <string name="test1">资源应用 test1</string>
    <string name="test2">资源应用 test2</string>
    <string name="test3">资源应用 test3</string>
    <string name="test4">资源应用 test4</string>
    <string name="test5">资源应用 test5</string>
    <string name="test6">资源应用 test6</string>
</resources>

  1. 然后在主应用,覆盖resource值为资源应用的resource
   //主应用 MainApp
    @Override
    public Resources getResources() {
        Context context = null;
        try {
            context = createPackageContext("com.jdf.res.resapp", Context.CONTEXT_IGNORE_SECURITY);
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }

        if (context != null) {
            return context.getResources();
        } else {
            return super.getResources();
        }
    }

通过以上方式覆盖后,我们再访问资源时,访问的就是资源应用的

Log.d("res", "Main app res string:" + getString(R.string.test1));
Log.d("res", "Main app res string:" + getString(R.string.test2));
Log.d("res", "Main app res string:" + getString(R.string.test3));
Log.d("res", "Main app res string:" + getString(R.string.test4));
Log.d("res", "Main app res string:" + getString(R.string.test5));
Log.d("res", "Main app res string:" + getString(R.string.test6));
  1. 测试结果
2010-01-01 08:37:02.153 8481-8481/com.jdf.res.main D/res: Main app res string:资源应用 test1
2010-01-01 08:37:02.156 8481-8481/com.jdf.res.main D/res: Main app res string:资源应用 test2
2010-01-01 08:37:02.157 8481-8481/com.jdf.res.main D/res: Main app res string:资源应用 test3
2010-01-01 08:37:02.159 8481-8481/com.jdf.res.main D/res: Main app res string:资源应用 test4
2010-01-01 08:37:02.161 8481-8481/com.jdf.res.main D/res: Main app res string:资源应用 test5
2010-01-01 08:37:02.162 8481-8481/com.jdf.res.main D/res: Main app res string:资源应用 test6

为什么能够访问到资源应用的字符串

原因是通过 android:sharedUserId将两个应用share到同一个进程,两个apk的同一个资源属性的资源id是相同的;然后通过覆盖resource,就能够通过资源id,访问到到另外一个应用的资源了

通过反编译,我们看下,两个应用的,新增的字符串属性的资源id编号

    //主应用的资源id
    /* renamed from: com.jdf.res.main.R$string */
    public static final class string {
        public static final int test1 = 2131492894;
        public static final int test2 = 2131492895;
        public static final int test3 = 2131492896;
        public static final int test4 = 2131492897;
        public static final int test5 = 2131492898;
        public static final int test6 = 2131492899;
    }

     //资源应用的资源id
    /* renamed from: com.jdf.res.resapp.R$string */
    public static final class string {
        .....
        public static final int test1 = 2131492894;
        public static final int test2 = 2131492895;
        public static final int test3 = 2131492896;
        public static final int test4 = 2131492897;
        public static final int test5 = 2131492898;
        public static final int test6 = 2131492899;
    }

这种方式,在xml引用资源,同样适用,因为资源文件,也是通过调用处的上下文ressource加载的。
有一个缺点,主应用MainApp的桌面app名称,还是应用自己的,而不是ResApp的。

下面我们针对其他应用获取应用方式,针对性的解决应用名称不能替换的问题

应用名称获取方式 1

    public static String getApplicationNameByPackageName(Context context, String packageName) {
        PackageManager pm = context.getPackageManager();
        String Name;
        try {
            Name = pm.getApplicationLabel(pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA)).toString();
        } catch (PackageManager.NameNotFoundException e) {
            Name = "";
        }
        return Name;
    }

如上所示,桌面是通过获取各个应用的getApplicationLabel获取应用名称的,所以,如果达到MainApp显示的应用名称也是ResApp的,我们还需要修改系统代码,覆盖MainApp的名称

系统修改修改方法:

diff --git a/base/core/java/android/app/ApplicationPackageManager.java b/base/core/java/android/app/ApplicationPackageManager.java

index b1a5651..40db061
--- a/base/core/java/android/app/ApplicationPackageManager.java
+++ b/base/core/java/android/app/ApplicationPackageManager.java
@@ -1755,7 +1755,10 @@ public class ApplicationPackageManager extends PackageManager {

     @Override
     public CharSequence getApplicationLabel(ApplicationInfo info) {
-        return info.loadLabel(this);
+        
+        return LabelManager.replaceLabel(this, info);
+       
+//        return info.loadLabel(this);
     }



应用名称获取方式2

    //LauncherActivityInfo.getLabel
    public CharSequence getLabel() {
        // TODO: Go through LauncherAppsService
        return mActivityInfo.loadLabel(mPm);
    }

    //PackageItemInfo
    public @NonNull CharSequence loadLabel(@NonNull PackageManager pm) {
        if (sForceSafeLabels) {
            return loadSafeLabel(pm);//里面也会调用loadUnsafeLabel方法
        } else {
            return loadUnsafeLabel(pm);
        }
    }

    /** {@hide} */
    public CharSequence loadUnsafeLabel(PackageManager pm) {
        if (nonLocalizedLabel != null) {
            CharSequence replacedLabel = LabelManager.loadUnsafeLabel(pm, packageName, labelRes, getApplicationInfo());
            return nonLocalizedLabel;
        }
        if (labelRes != 0) {
            CharSequence label = pm.getText(packageName, labelRes, getApplicationInfo());
            if (label != null) {
                return label.toString().trim();
            }
        }
        if (name != null) {
            return name;
        }
        return packageName;
    }

考虑到子类覆盖的情况

xxx@ubuntu:~/xxx/android/xxx/frameworks/base/core/java$ grep -nr "loadUnsafeLabel(PackageManager pm)"
android/content/pm/ComponentInfo.java:100:    @Override public CharSequence loadUnsafeLabel(PackageManager pm) {
android/content/pm/PackageItemInfo.java:199:    public CharSequence loadUnsafeLabel(PackageManager pm) {

需要对ComponentInfo.java和PackageItemInfo.java两个类的loadUnsafeLabel方法进行处理

   //ComponentInfo.java
    /** @hide */
    @Override public CharSequence loadUnsafeLabel(PackageManager pm) {
        if (nonLocalizedLabel != null) {
            return nonLocalizedLabel;
        }
        ApplicationInfo ai = applicationInfo;
        CharSequence label;

         if (labelRes != 0) {
-            label = pm.getText(packageName, labelRes, ai);
+            /* add for res replace */
+            label = LabelManager.loadUnsafeLabel(pm, packageName, labelRes, ai);
+            //label = pm.getText(packageName, labelRes, ai);
             if (label != null) {
                 return label;
             }
@@ -126,7 +130,10 @@ public class ComponentInfo extends PackageItemInfo {
             return ai.nonLocalizedLabel;
         }
         if (ai.labelRes != 0) {
-            label = pm.getText(packageName, ai.labelRes, ai);
+            /* add for res replace */
+            label = LabelManager.loadUnsafeLabel(pm, packageName, ai.labelRes, ai);
+            //label = pm.getText(packageName, ai.labelRes, ai);
             if (label != null) {
                 return label;
             }

    //PackageItemInfo.java
    /** {@hide} */
    public CharSequence loadUnsafeLabel(PackageManager pm) {
        if (nonLocalizedLabel != null) {
            return nonLocalizedLabel;
        }
         if (labelRes != 0) {
-            CharSequence label = pm.getText(packageName, labelRes, getApplicationInfo());
+            /* add for camera res replace */
+            CharSequence label = LabelManager.loadUnsafeLabel(pm, packageName, labelRes, getApplicationInfo());
+            //CharSequence label = pm.getText(packageName, labelRes, getApplicationInfo());
             if (label != null) {
                 return label.toString().trim();
             }

LabelManager的实现

package android.app;

import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.util.Log;

public class LabelManager {
    public static final String TAG = "LabelManager";

    public static final boolean IS_SUPPORT_LABEL_MAR = true;

    private static final String[] REPLACE_FROM_PKGS = new String[] {
            "com.jdf.res.main",
            "com.android.camera" 
            };

    private static final String[] REPLACE_TO_PKGS = new String[] {
            "com.jdf.res.resapp",
            "test.android.camera"
            };

    private static String getReplacedPkg(String pkg) {
        if (pkg != null) {
            for (int i = 0; i < REPLACE_FROM_PKGS.length; i++) {
                if (pkg.equals(REPLACE_FROM_PKGS[i])) {
                    return REPLACE_TO_PKGS[i];
                }
            }
        }
        return null;
    }
    
    
    public static CharSequence replaceLabel(PackageManager pm, ApplicationInfo info) {
        if (IS_SUPPORT_LABEL_MAR) {
            final String replacedPkg = getReplacedPkg(info.packageName);
            if (replacedPkg != null) {//属于label要被替代的应用
                ApplicationInfo replacedInfo;
                try {
                    replacedInfo = pm.getApplicationInfo(replacedPkg, PackageManager.GET_META_DATA);
                    if (replacedInfo != null) {
                        final CharSequence loadLabel = replacedInfo.loadLabel(pm);
                        Log.d(TAG, "replace from [" + info.packageName + "] to [" + replacedPkg + "]");
                        Log.d(TAG, "replace from [" + info.loadLabel(pm) + "] to [" + loadLabel + "]");

                        return loadLabel;
                    }
                } catch (NameNotFoundException e) {
                    e.printStackTrace();
                }

            }
        }
        return info.loadLabel(pm);
    }

    public static CharSequence loadUnsafeLabel(PackageManager pm, String fromPkgName, int labelRes,
            ApplicationInfo appinfo) {
        String toPkgName = getReplacedPkg(fromPkgName);
        Log.d(LabelManager.TAG, "loadUnsafeLabel toPkgName:" +toPkgName);

        if (toPkgName != null) {//改应用属于是要被替代的应用
            ApplicationInfo applicationInfo = null;
            try {
                //使用替代的ApplicationInfo
                applicationInfo = pm.getApplicationInfo(toPkgName, PackageManager.GET_META_DATA);
            } catch (NameNotFoundException e) {
                e.printStackTrace();
            }
            Log.d(LabelManager.TAG, "loadUnsafeLabel applicationInfo:" +applicationInfo);

            if (applicationInfo != null) {
                //获取目标App的应用名称
                CharSequence toLabel = pm.getText(toPkgName, labelRes, applicationInfo);
                Log.d(LabelManager.TAG, "loadUnsafeLabel replace[" + fromPkgName + "] to " + toPkgName + " label:" + toLabel);
                if (toLabel != null) {
                    return toLabel.toString().trim();
                }
            }
        }
        return pm.getText(fromPkgName, labelRes, appinfo);
    }

}

应用名称映射效果图

应用名称替换效果.png

解决方案 2

通过动态资源覆盖的方式,实现,参考章节:

优缺点:

方案一:

优点:每次修改资源,只需修改和编译ResApp,方便改动和发布
缺点:ResApp中的资源文件属性,需要跟MainApp保持一次,不能删除和增加属性字段;
需要修改系统,虽然改动不大

方案二:

优点:只需添加要覆盖的资源文件即可
缺点:每次资源改动,需要重新编译系统,改动和发布比较麻烦

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容

  • feisky云计算、虚拟化与Linux技术笔记posts - 1014, comments - 298, trac...
    不排版阅读 3,750评论 0 5
  • 简述:    一直希望有个机会可以好好研究一下android手机的多主题功能,借此机会将自己所能分析到的内容记录...
    大天使之剑阅读 1,656评论 0 6
  • 1.介绍 如果你正在查阅build.gradle文件的所有可选项,请点击这里进行查阅:DSL参考 1.1新构建系统...
    Chuckiefan阅读 11,991评论 8 72
  • Author:杨空明 Date:2018-8-17 一、前言 Android开发者常常面临的一个问题就是防破解、 ...
    问心2018阅读 27,836评论 4 66
  • 往复读书梦,挑灯夜未央 (作者:张倩倩) 岁月安然的日子里 我曾把在路上的风景 点上未央的雅名 我把读书的的梦 缀...
    红巧儿阅读 29评论 0 1