非默认应用删除短信失败 -- ContentProvider.delete分析

使用 ContentResolver.delete()时, 来看看流程.

在 ContentResolver 来进行删除时, 会调用 ContentProvider 的方法. 也就说明了. 使用 ContentProvider 时, 实际的数据操作在 ContentProvider, 而 ContentProvider 仅仅相当于一个接口.

  • ContentResolver.java
public final int delete(@RequiresPermission.Write @NonNull Uri url, @Nullable String where,
            @Nullable String[] selectionArgs) {
        ...
        try {
            long startTime = SystemClock.uptimeMillis();
            int rowsDeleted = provider.delete(mPackageName, url, where, selectionArgs);// to ContentProvider
            long durationMillis = SystemClock.uptimeMillis() - startTime;
            maybeLogUpdateToEventLog(durationMillis, url, "delete", where);
            return rowsDeleted;
        }catch{} 
        ...
        }
    }

其向 ContentProvider 传入了四个参数:
callingPkg: 使用者包名

uri: uri 路径

selection: 条件

selectionArgs: 条件参数 

进入ContentProvider.delete, 可以发现, 其调用了 SQLiteContentProvider. 所以, 其实他们也就是对数据库进行了处理, 也就是其封装类.

  • ContentProvider.java
   public int delete(String callingPkg, Uri uri, String selection, String[] selectionArgs) {
            validateIncomingUri(uri);
            uri = getUriWithoutUserId(uri);
            if (enforceWritePermission(callingPkg, uri, null) != AppOpsManager.MODE_ALLOWED) {
                return 0;
            }
            final String original = setCallingPackage(callingPkg);
            try {
                return ContentProvider.this.delete(uri, selection, selectionArgs);//SQLiteContentProvider.java
            } finally {
                setCallingPackage(original);
            }
        }

看其中, if语句:

  • ContentProvider.java
    if (enforceWritePermission(callingPkg, uri, null) !=AppOpsManager.MODE_ALLOWED) {
        return 0;
    }
    
    // enforceWritePermission 为false时, 那么返回为 "零". 
    //如果未被删除, 那么返回值为 "零", 且不会报错.

也就是说: 如果权限的验证返回值不为 : AppOpsManager.MODE_ALLOWED, 就得不到我们想要的结果. 那么我们仅仅分析权限这个问题, 看起需要满足什么样的条件才能保证我们得到正确的返回值.


权限的验证会到达:

  • ContentProvider.java
    protected int enforceReadPermissionInner(Uri uri, String callingPkg, IBinder callerToken) throws SecurityException {
        ...
        if (mExported && checkUser(pid, uid, context)) {
            final String componentPerm = getReadPermission();
            if (componentPerm != null) {
                final int mode = checkPermissionAndAppOp(componentPerm, callingPkg, callerToken);
                if (mode == MODE_ALLOWED) {
                    return MODE_ALLOWED;
                } else {
                    missingPerm = componentPerm;
                    strongestMode = Math.max(strongestMode, mode);
                }
            }

            // track if unprotected read is allowed; any denied
            // <path-permission> below removes this ability
            boolean allowDefaultRead = (componentPerm == null);

            final PathPermission[] pps = getPathPermissions();
            if (pps != null) {
                final String path = uri.getPath();
                for (PathPermission pp : pps) {
                    final String pathPerm = pp.getReadPermission();
                    if (pathPerm != null && pp.match(path)) {
                        final int mode = checkPermissionAndAppOp(pathPerm, callingPkg, callerToken);
                        if (mode == MODE_ALLOWED) {
                            return MODE_ALLOWED;
                        } 
                        ...
                    }
                }
            }
            ...
        }
        ...
    }


从这里发现, 在验证权限时, 还需要做一步操作. 这个方法会将传进来的 "读写权限", "包名", IBinder对象传入, 返回一个mode.

final int mode = checkPermissionAndAppOp(componentPerm, callingPkg, callerToken);

  • ContentProvider.java
private int checkPermissionAndAppOp(String permission, String callingPkg,
            IBinder callerToken) {
        ...
        final int permOp = AppOpsManager.permissionToOpCode(permission);
        if (permOp != AppOpsManager.OP_NONE) {
            return mTransport.mAppOpsManager.noteProxyOp(permOp, callingPkg);
        }

        return MODE_ALLOWED;
    }

这个方法传进去了三个参数:

 permission: "读" 或 "写" 的权限.

 callingPkg: 从 ContentResolver 就开始了传入.

 IBInder: null

先看

final int permOp = AppOpsManager.permissionToOpCode(permission);

  • AppOpsManager.java
   public static int permissionToOpCode(String permission) {
        Integer boxedOpCode = sRuntimePermToOp.get(permission);
        return boxedOpCode != null ? boxedOpCode : OP_NONE;
    }
//这里的 sRuntimePermToOp 是一个Map对象, 它们其实是 int sOpPerms[] - 数组角标的key - value.

而 OP_NONE =-1, 表示不在数组中. 如果在数组中, 那么:

  • ContentProvider.java
    if (permOp != AppOpsManager.OP_NONE) {
        return mTransport.mAppOpsManager.noteProxyOp(permOp, callingPkg);
    }


那么此时就可以分析:

mTransport.mAppOpsManager.noteProxyOp(permOp, callingPkg);

  • AppOpsManager.java
    public int noteProxyOp(int op, String proxiedPackageName) {
        int mode = noteProxyOpNoThrow(op, proxiedPackageName);
        if (mode == MODE_ERRORED) {
            throw new SecurityException("Proxy package " + mContext.getOpPackageName()
                    + " from uid " + Process.myUid() + " or calling package "
                    + proxiedPackageName + " from uid " + Binder.getCallingUid()
                    + " not allowed to perform " + sOpNames[op]);
        }
        return mode;
    }
    
    public int noteProxyOpNoThrow(int op, String proxiedPackageName) {
        try {
            return mService.noteProxyOperation(op, mContext.getOpPackageName(),
                    Binder.getCallingUid(), proxiedPackageName);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

刚进入 AppOpsManager, 才仅仅做了两次跳转. 直接到 AppOpsService 中去处理了.

  •  AppOpsService.java
 public int noteProxyOperation(int code, String proxyPackageName,
            int proxiedUid, String proxiedPackageName) {
        verifyIncomingOp(code); /验证权限是否正确.
        final int proxyUid = Binder.getCallingUid();
        //通过uid来判断其所属的进程.
        
        /*
        private static String resolvePackageName(int uid, String packageName)  {
            if (uid == 0) {
                return "root";
            } else if (uid == Process.SHELL_UID) {
                return "com.android.shell";
            } else if (uid == Process.SYSTEM_UID && packageName == null) {
                return "android";
            }
            return packageName;
        }
        */
        
        //可以看出, 其返回的有四类值. 
        //当然需要注意的是 proxyPackageName 这个并不是我们自己应用的包名.
        
        String resolveProxyPackageName = resolvePackageName(proxyUid, proxyPackageName);
        ...
        final int proxyMode = noteOperationUnchecked(code, proxyUid,
                resolveProxyPackageName, -1, null);
        if (proxyMode != AppOpsManager.MODE_ALLOWED || Binder.getCallingUid() == proxiedUid) {
            return proxyMode;
        }
        String resolveProxiedPackageName = resolvePackageName(proxiedUid, proxiedPackageName);
        if (resolveProxiedPackageName == null) {
            return AppOpsManager.MODE_IGNORED;
        }
        return noteOperationUnchecked(code, proxiedUid, resolveProxiedPackageName,
                proxyMode, resolveProxyPackageName);
    }


> code : int permOp = AppOpsManager.permissionToOpCode(permission); 返回值

> proxyPackageName: 代理包名, 即 AppOpsManager 的包名

> proxiedUid: Binder.getCallingUid(), 在AppOpsManager中被调用. 

> proxiedPackageName: ContentResolver的包名

如果满足这个条件:



    if (proxyMode != AppOpsManager.MODE_ALLOWED || Binder.getCallingUid() == proxiedUid) {
        return proxyMode;
    }
 


那么, 返回值不为: AppOpsManager.MODE_ALLOWED. 这就表示 ContentResovle.delte的返回值为零, 得不到我们想要的结果. 

其实, 从这里就可以得到上面的总结:

> ContentResolve.delete 的操作, 其实调用了ContentProvider.delete, 并最终访问了数据库. 说明它们其实就是对数据库进行操作.

> ContentResolve, ContentProvider 实际是对数据库操作的封装.

> ContentResolve, ContentProvider 在进行"增", "删", "改", "查"时, 会进行权限的验证. 

如果该应用走的是

return noteOperationUnchecked(code, proxiedUid, resolveProxiedPackageName,
                proxyMode, resolveProxyPackageName);
                
AppOpsService.java

> code: 权限数组的的值, 如果为 MODE_ALLOWED, 即表示有权限.

> uid: proxyUid, 在 AppOpsManager中使用 Binder.getCallingUid获得.

> packageName:  resolvePackageName(proxiedUid, proxiedPackageName) 的返回值. 这里的 proxiedPackageName, 是用户应用apk的包名

> proxyUid: noteOperationUnchecked(code, proxyUid,resolveProxyPackageName, -1, null)的返回值.

> proxyPackageName: resolvePackageName(proxyUid, proxyPackageName)的返回值. 这里是AppOpsManager中所调用Binder.getCallingPkg()所获得的包名.

其实从这里就基本可以确认了:

> 权限的验证的流程从 ContentProvider -> AppOpsManager -> AppOpsService.

> 权限的验证会最终到AppOpsService.noteOperationUnchecked.

-----------------------------------------------------------------------------

那么应用的权限是如何被赋予的呢?

以短信应用为例. 如果用户短信应用不是默认的短信应用, 那么其在使用 ContentProvider 的"增", "删", "改", "查" 的操作时, 无法做出正确的响应. 

设置短信应用为默认的短信应用的方法为 SmsApplication.setDefaultApplication().

public static void setDefaultApplication(String packageName, Context context) {
        TelephonyManager tm = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
        if (!tm.isSmsCapable()) {
            // No phone, no SMS
            return;
        }

        final int userId = getIncomingUserId(context);
        final long token = Binder.clearCallingIdentity();
        try {
            setDefaultApplicationInternal(packageName, context, userId);
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }

这个类没有提过外部接口, 可以使用反射机制来调用该方法, 为我们自己的应用设置成默认应用.

从 SmsApplication 的设置默认 SMS 的操作:

# SmsApplication.java

public static void setDefaultApplication(String packageName, Context context) {
        TelephonyManager tm = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
        if (!tm.isSmsCapable()) {
            // No phone, no SMS
            return;
        }

        final int userId = getIncomingUserId(context);
        final long token = Binder.clearCallingIdentity();
        try {
            setDefaultApplicationInternal(packageName, context, userId);
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }

// 设置默认短信应用的实际方法在: setDefaultApplicationInternal().

> packageName: 传进来的默认包名.

// 这个方法主要切换新旧的"默认包" 来切换使用默认新的应用包名.

    private static void setDefaultApplicationInternal(String packageName, Context context, int userId) {
        // Get old package name
        String oldPackageName = Settings.Secure.getStringForUser(context.getContentResolver(),
                Settings.Secure.SMS_DEFAULT_APPLICATION, userId);
        ...

        if (packageName != null && oldPackageName != null && packageName.equals(oldPackageName)) {
            // No change
            return;
        }

        // We only make the change if the new package is valid
        PackageManager packageManager = context.getPackageManager();
        Collection<SmsApplicationData> applications = getApplicationCollection(context);
        SmsApplicationData oldAppData = oldPackageName != null ?
                getApplicationForPackage(applications, oldPackageName) : null;
        SmsApplicationData applicationData = getApplicationForPackage(applications, packageName);
        if (applicationData != null) {
            // Ignore OP_WRITE_SMS for the previously configured default SMS app.
            AppOpsManager appOps = (AppOpsManager)context.getSystemService(Context.APP_OPS_SERVICE);
            if (oldPackageName != null) {
                try {
                    PackageInfo info = packageManager.getPackageInfo(oldPackageName,
                            PackageManager.GET_UNINSTALLED_PACKAGES);
                    //设置 MODE_IGNORED, 表明 oldpackage 将不具备处理短信的功能.
                    appOps.setMode(AppOpsManager.OP_WRITE_SMS, info.applicationInfo.uid,
                            oldPackageName, AppOpsManager.MODE_IGNORED);
                } catch (NameNotFoundException e) {
                    Rlog.w(LOG_TAG, "Old SMS package not found: " + oldPackageName);
                }
            }
            //将传进来的 package name 作为新的默认程序来进行处理.
            // Update the secure setting.
            Settings.Secure.putStringForUser(context.getContentResolver(),
                    Settings.Secure.SMS_DEFAULT_APPLICATION, applicationData.mPackageName,
                    userId);

            // Configure this as the preferred activity for SENDTO sms/mms intents
            configurePreferredActivity(packageManager, new ComponentName(
                    applicationData.mPackageName, applicationData.mSendToClass), userId);
            // 设置 AppOpsManager.MODE_ALLOWED 权限.
            // Allow OP_WRITE_SMS for the newly configured default SMS app.
            appOps.setMode(AppOpsManager.OP_WRITE_SMS, applicationData.mUid,
                    applicationData.mPackageName, AppOpsManager.MODE_ALLOWED);

            // Assign permission to special system apps
            assignWriteSmsPermissionToSystemApp(context, packageManager, appOps,
                    PHONE_PACKAGE_NAME);
            assignWriteSmsPermissionToSystemApp(context, packageManager, appOps,
                    BLUETOOTH_PACKAGE_NAME);
            assignWriteSmsPermissionToSystemApp(context, packageManager, appOps,
                    MMS_SERVICE_PACKAGE_NAME);
            assignWriteSmsPermissionToSystemApp(context, packageManager, appOps,
                    TELEPHONY_PROVIDER_PACKAGE_NAME);

          
            // Give WRITE_SMS AppOps permission to UID 1001 which contains multiple
            // apps, all of them should be able to write to telephony provider.
            // This is to allow the proxy package permission check in telephony provider
            // to pass.
            assignWriteSmsPermissionToSystemUid(appOps, Process.PHONE_UID);

            ...
            //在移除 oldpacakge的作为默认的设置
            if (oldAppData != null && oldAppData.mSmsAppChangedReceiverClass != null) {
                // Notify the old sms app that it's no longer the default
                final Intent oldAppIntent =
                        new Intent(Telephony.Sms.Intents.ACTION_DEFAULT_SMS_PACKAGE_CHANGED);
                final ComponentName component = new ComponentName(oldAppData.mPackageName,
                        oldAppData.mSmsAppChangedReceiverClass);
                oldAppIntent.setComponent(component);
                oldAppIntent.putExtra(Telephony.Sms.Intents.EXTRA_IS_DEFAULT_SMS_APP, false);
                if (DEBUG_MULTIUSER) {
                    Log.i(LOG_TAG, "setDefaultApplicationInternal old=" + oldAppData.mPackageName);
                }
                context.sendBroadcast(oldAppIntent);
            }
            
            // Notify the new sms app that it's now the default (if the new sms app has a receiver
            // to handle the changed default sms intent).
            if (applicationData.mSmsAppChangedReceiverClass != null) {
                final Intent intent =
                        new Intent(Telephony.Sms.Intents.ACTION_DEFAULT_SMS_PACKAGE_CHANGED);
                final ComponentName component = new ComponentName(applicationData.mPackageName,
                        applicationData.mSmsAppChangedReceiverClass);
                intent.setComponent(component);
                intent.putExtra(Telephony.Sms.Intents.EXTRA_IS_DEFAULT_SMS_APP, true);
                context.sendBroadcast(intent);
            }
            MetricsLogger.action(context, MetricsEvent.ACTION_DEFAULT_SMS_APP_CHANGED,
                    applicationData.mPackageName);
        }
    }
    


从这个方法可以看出:
> 系统在设置默认短信应用时, 需要冠以权限, 和设置默认短信两部操作.

> 如果之前有默认短信应用, 先将其应用去掉权限, 并从广播中移除. 然后在将新的短信应用设置成默认的应用并冠以权限.

这里可以思考一个问题. 为自制的短信应用设置成默认短信应用. 则可以对 SmsApplication.setDefaultApplication 进行重写.

由于 SmsApplication 没有对外提供接口, 可考虑使用 反射机制来处理.

public static void setAsDefaultApp(String packageName, Context ctx) {
    try {
        Class<?> systemClass = Class.forName("com.android.internal.telephony.SmsApplication");
            Method myMethod = systemClass.getMethod("setDefaultApplication", String.class, Context.class);
            Method[] allMethod = systemClass.getDeclaredMethods();
            Constructor[] cons = systemClass.getDeclaredConstructors();
            cons[0].getName();
            cons[0].setAccessible(true);
            Object Tmngr = cons[0].newInstance();

            for (Method m : allMethod) {
                if (m.getName().toUpperCase().equalsIgnoreCase("setDefaultApplication".toUpperCase())) {
                    Object o[] = new Object[2];
                    o[0] = packageName;
                    o[1] = ctx.getApplicationContext();
                    try {
                        m.setAccessible(true);
                        m.invoke(Tmngr, o);
                    } catch (Exception e1) {
                        Log.e("test", "-->setAsDefaultApp-->setDefaultApplication:" + e1.getMessage());
                    }
                }
            }

//            Settings.Secure.putString(ctx.getContentResolver(), Settings.Secure.SMS_DEFAULT_APPLICATION, packageName);
//            myMethod.invoke(null,packageName, ctx);
        
        } catch (Exception e) {
    
        }
    }


这样就可以使自制短信应用具有 默认短信应用的全新, 可以和系统自带的短信应用做"增", "删", "改", "查"等需要权限的处理了.

-----------------------------------------------------------------------------

当然, 还有一种方式, 就是为该应用设置权限: AppOpsManager.MODE_ALLOWED.

SmsApplication.setDefaultApplicationInternal

appOps.setMode(AppOpsManager.OP_WRITE_SMS, applicationData.mUid,
                    applicationData.mPackageName, AppOpsManager.MODE_ALLOWED);


这个代码就是为应用设置了该权限.

先找到该方法的实现:

  • AppOpsManager.java
    public void setMode(int code, int uid, String packageName, int mode) {
        try {
            mService.setMode(code, uid, packageName, mode);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

实际上, 该方法的具体实现在 AppOpsService, 具体的源码就不贴出来了. 因为现在 可以考虑设置权限的问题了. 依然使用反射机制

public static final int OP_WRITE_SMS = 15;

> code=15;

    private static boolean setMode(Context context, int code, int uid, int mode) {
        AppOpsManager appOpsManager = (AppOpsManager) context
                .getSystemService(Context.APP_OPS_SERVICE);
        Class appOpsManagerClass = appOpsManager.getClass();

        try {
            Class[] types = new Class[4];
            types[0] = Integer.TYPE;
            types[1] = Integer.TYPE;
            types[2] = String.class;
            types[3] = Integer.TYPE;
            Method setModeMethod = appOpsManagerClass.getMethod("setMode",
                    types);

            Object[] args = new Object[4];
            args[0] = Integer.valueOf(code);
            args[1] = Integer.valueOf(uid);
            args[2] = context.getPackageName();
            args[3] = Integer.valueOf(mode);
            setModeMethod.invoke(appOpsManager, args);

            return true;
        } catch (Exception e) {
            e.printStackTrace();
        } 
        return false;
    }


这样就在进行删除操作的时候, 进行上述两种操作, 就可以付上其相应的权限.

现在再进一步完善代码, 进行一次容错处理. 如果应用 "已经是默认短信应用" 或者 "已经设置了默认权限" .

首先针对第一个容错处理: 已经是默认短信应用

  • SmsApplication.java
    public static boolean isDefaultSmsApplication(Context context, String packageName) {
        if (packageName == null) {
            return false;
        }
        final String defaultSmsPackage = getDefaultSmsApplicationPackageName(context);
        if ((defaultSmsPackage != null && defaultSmsPackage.equals(packageName))
                || BLUETOOTH_PACKAGE_NAME.equals(packageName)) {
            return true;
        }
        
        return false;
    }

只要反射该方法, 就可以了.

-----------------------------------------------------------------------------
额外指出, 这个方法的说明是: 是否需要向数据库写入 sms. 如果不向数据库写入数据, 那么 sms 将不会显示在界面上. 自行思考其用处.

  • SmsApplication.java
    /**
     * Returns whether need to write the SMS message to SMS database for this package.
     * <p>
     * Caller must pass in the correct user context if calling from a singleton service.
     */
    public static boolean shouldWriteMessageForPackage(String packageName, Context context) {
        if (SmsManager.getDefault().getAutoPersisting()) {
            return true;
        }
    if((packageName != null) && (packageName.equals("com.ape.saletracker") || packageName.equals("com.saleTrackPK"))){
        return false;
    }

        return !isDefaultSmsApplication(context, packageName);
    }


----------------------------------------------------------------------------


再来对第二种方式进行容错处理: "已经设置了默认权限".

AppOpsManager.java

    public int checkOp(String op, int uid, String packageName) {
        return checkOp(strOpToOp(op), uid, packageName);
    }

   public int checkOp(int op, int uid, String packageName) {
        try {
            int mode = mService.checkOperation(op, uid, packageName);
            if (mode == MODE_ERRORED) {
                throw new SecurityException(buildSecurityExceptionMsg(op, uid, packageName));
            }
            return mode;
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

这里的两个方法的处理方式, 一模一样, 只是传入的的参数类型不同. 

  • SmsApplication.java

int mode = appOps.checkOp(AppOpsManager.OP_WRITE_SMS, info.applicationInfo.uid, packageName);

根据上面这个来仿写:

    private static Object checkOp(Context context, int code, int uid) {
        AppOpsManager appOpsManager = (AppOpsManager) context
                .getSystemService(Context.APP_OPS_SERVICE);
        Class appOpsManagerClass = appOpsManager.getClass();

        try {
            Class[] types = new Class[3];
            types[0] = Integer.TYPE;
            types[1] = Integer.TYPE;
            types[2] = String.class;
            Method checkOpMethod = appOpsManagerClass.getMethod("checkOp",
                    types);

            Object[] args = new Object[3];
            args[0] = Integer.valueOf(code);//public static final int OP_WRITE_SMS = 15;
            args[1] = Integer.valueOf(uid);
            args[2] = context.getPackageName();
            Object result = checkOpMethod.invoke(appOpsManager, args);

            return result;
        } catch (Exception e) {
            e.printStackTrace();
        } 
        return null;
    }


 

猜你喜欢

转载自blog.csdn.net/dec_sun/article/details/82706169