android 4.4之后非默认的短信应用已经没有办法删除短信了。像以前那样用如下方法是不会没法删除短信的(即使在xml中配置了短信的读写权限),同时也不会有报错或其他提示。
public void deleteSMS() {
try {
ContentResolver CR = getContentResolver();
// Query SMS
Uri uriSms = Uri.parse("content://sms/inbox");
Cursor c = CR.query(uriSms, new String[] { "_id", "thread_id" },
null, null, null);
if (null != c && c.moveToFirst()) {
do {
// Delete SMS
long threadId = c.getLong(1);
int result = CR.delete(Uri
.parse("content://sms/conversations/" + threadId),
null, null);
Log.d("deleteSMS", "threadId:: " + threadId + " result::"
+ result);
} while (c.moveToNext());
}
} catch (Exception e) {
Log.d("deleteSMS", "Exception:: " + e);
}
}
但通过打印可以看到上述代码的result是等于0的,即没有删除掉短信。
这个是因为在:/frameworks/base/services/java/com/android/server/AppOpsService.java中android系统添加了权限检查的函数
检查用户设定权限的函数是:checkOperation() 和 noteOperation(),区别是 checkOperation() 只是检查 Operation 的情况,noteOperation() 还会记录访问时间等信息,代码如下:
@Override public int checkOperation(int code, int uid, String packageName) { verifyIncomingUid(uid); verifyIncomingOp(code); synchronized (this) { Op op = getOpLocked(AppOpsManager.opToSwitch(code), uid, packageName, false); if (op == null) { return AppOpsManager.MODE_ALLOWED; } return op.mode; } } @Override public int noteOperation(int code, int uid, String packageName) { verifyIncomingUid(uid); verifyIncomingOp(code); synchronized (this) { Ops ops = getOpsLocked(uid, packageName, true); if (ops == null) { if (DEBUG) Log.d(TAG, "noteOperation: no op for code " + code + " uid " + uid + " package " + packageName); return AppOpsManager.MODE_IGNORED; } Op op = getOpLocked(ops, code, true); if (op.duration == -1) { Slog.w(TAG, "Noting op not finished: uid " + uid + " pkg " + packageName + " code " + code + " time=" + op.time + " duration=" + op.duration); } op.duration = 0; final int switchCode = AppOpsManager.opToSwitch(code); final Op switchOp = switchCode != code ? getOpLocked(ops, switchCode, true) : op; if (switchOp.mode != AppOpsManager.MODE_ALLOWED) { if (DEBUG) Log.d(TAG, "noteOperation: reject #" + op.mode + " for code " + switchCode + " (" + code + ") uid " + uid + " package " + packageName); op.rejectTime = System.currentTimeMillis(); return switchOp.mode; } if (DEBUG) Log.d(TAG, "noteOperation: allowing code " + code + " uid " + uid + " package " + packageName); op.time = System.currentTimeMillis(); op.rejectTime = 0; return AppOpsManager.MODE_ALLOWED; } }
然后在MmsServiceBroker服务中可以找到如下代码就是对应用删除短信的权限进行检查
@Override
public boolean deleteStoredMessage(String callingPkg, Uri messageUri)
throws RemoteException {
mContext.enforceCallingPermission(Manifest.permission.WRITE_SMS,
"Delete SMS/MMS message");
if (getAppOpsManager().noteOp(AppOpsManager.OP_WRITE_SMS, Binder.getCallingUid(),
callingPkg) != AppOpsManager.MODE_ALLOWED) {
return false;
}
return getServiceGuarded().deleteStoredMessage(callingPkg, messageUri);
}
@Override
public boolean deleteStoredConversation(String callingPkg, long conversationId)
throws RemoteException {
mContext.enforceCallingPermission(Manifest.permission.WRITE_SMS, "Delete conversation");
if (getAppOpsManager().noteOp(AppOpsManager.OP_WRITE_SMS, Binder.getCallingUid(),
callingPkg) != AppOpsManager.MODE_ALLOWED) {
return false;
}
return getServiceGuarded().deleteStoredConversation(callingPkg, conversationId);
}
不过幸运的是在AppOpsService.java中也提供了修改权限的接口:修改某个 App 的某项权限的函数是 setMode(),其中就是修改成员变量 mUidOps。mUidOps 是一个List 保存了某个package对应的所有权限的mode (允许,忽略),具体代码如下:
@Override public void setMode(int code, int uid, String packageName, int mode) { verifyIncomingUid(uid); verifyIncomingOp(code); ArrayList<Callback> repCbs = null; code = AppOpsManager.opToSwitch(code); synchronized (this) { Op op = getOpLocked(code, uid, packageName, true); if (op != null) { if (op.mode != mode) { op.mode = mode; ArrayList<Callback> cbs = mOpModeWatchers.get(code); if (cbs != null) { if (repCbs == null) { repCbs = new ArrayList<Callback>(); } repCbs.addAll(cbs); } cbs = mPackageModeWatchers.get(packageName); if (cbs != null) { if (repCbs == null) { repCbs = new ArrayList<Callback>(); } repCbs.addAll(cbs); } if (mode == AppOpsManager.MODE_ALLOWED) { // If going into the default mode, prune this op // if there is nothing else interesting in it. if (op.time == 0 && op.rejectTime == 0) { Ops ops = getOpsLocked(uid, packageName, false); if (ops != null) { ops.remove(op.op); if (ops.size() <= 0) { HashMap<String, Ops> pkgOps = mUidOps.get(uid); if (pkgOps != null) { pkgOps.remove(ops.packageName); if (pkgOps.size() <= 0) { mUidOps.remove(uid); } } } } } } scheduleWriteNowLocked(); } } } if (repCbs != null) { for (int i=0; i<repCbs.size(); i++) { try { repCbs.get(i).mCallback.opChanged(code, packageName); } catch (RemoteException e) { } } } }
AppOpsManager 是一个管理类来和 AppOpsService 通信,两者关联起来的代码如下:
/**
* Common implementation of Context API, which provides the base
* context object for Activity and other application components.
*/
class ContextImpl extends Context {
registerService(WINDOW_SERVICE, new ServiceFetcher() { Display mDefaultDisplay; public Object getService(ContextImpl ctx) { Display display = ctx.mDisplay; if (display == null) { if (mDefaultDisplay == null) { DisplayManager dm = (DisplayManager)ctx.getOuterContext(). getSystemService(Context.DISPLAY_SERVICE); mDefaultDisplay = dm.getDisplay(Display.DEFAULT_DISPLAY); } display = mDefaultDisplay; } return new WindowManagerImpl(display); }});
.....
}
他的函数实现比较简单,重点是把控制转移到 AppOpsService 就可以了。例如 noteOperation() 和 setMode() 在 AppOpsManager 里面调用他们的函数是 noteOp() 和 setMode(),代码如下:
public int noteOp(int op, int uid, String packageName) { try { int mode = mService.noteOperation(op, uid, packageName); if (mode == MODE_ERRORED) { throw new SecurityException("Operation not allowed"); } return mode; } catch (RemoteException e) { } return MODE_IGNORED; } public void setMode(int code, int uid, String packageName, int mode) { try { mService.setMode(code, uid, packageName, mode); } catch (RemoteException e) { } }
OK,到这里我们应该就能有解决方法了,虽然接口没有公开,但我们在apk中利用反射来调用AppOpsManager,再利用setMode方法来给自己的应用打开权限,代码如下:
public final class SmsWriteOpUtil {
private static final int OP_WRITE_SMS = 15;
public static boolean isWriteEnabled(Context context) {
int uid = getUid(context);
Object opRes = checkOp(context, OP_WRITE_SMS, uid);
if (opRes instanceof Integer) {
return (Integer) opRes == AppOpsManager.MODE_ALLOWED;
}
return false;
}
public static boolean setWriteEnabled(Context context, boolean enabled) {
int uid = getUid(context);
int mode = enabled ? AppOpsManager.MODE_ALLOWED
: AppOpsManager.MODE_IGNORED;
return setMode(context, OP_WRITE_SMS, uid, mode);
}
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);
args[1] = Integer.valueOf(uid);
args[2] = context.getPackageName();
Object result = checkOpMethod.invoke(appOpsManager, args);
return result;
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
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 (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return false;
}
private static int getUid(Context context) {
try {
int uid = context.getPackageManager().getApplicationInfo(
context.getPackageName(), PackageManager.GET_SERVICES).uid;
return uid;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return 0;
}
}
}
使用起起来也很方便:
if (!SmsWriteOpUtil.isWriteEnabled(getApplicationContext())) { SmsWriteOpUtil.setWriteEnabled( getApplicationContext(), true); } deleteSMS();
......
注意还别忘:
<uses-permission android:name="android.permission.READ_SMS" /> <uses-permission android:name="android.permission.WRITE_SMS" />