Android OS

Android 应用间资源覆盖

2020-01-19  本文已影响0人  古风子

该方案基于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保持一次,不能删除和增加属性字段;
需要修改系统,虽然改动不大

方案二:

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

上一篇下一篇

猜你喜欢

热点阅读