Android8.0中实现APP禁用模式(一)

需求

产品经理要求在Android平板中实现一个应用的禁用模式。当一个已安装的应用被设置为禁用的时候,在启动器中,APP图标灰色,且APP不能启动。

需求分析

打开一个APP的方式有三种:1、从启动器点击图标启动;2、点击APP弹出的通知启动;3、点击多任务键,选择APP。这三种启动方式中,第一个很容易实现禁用,只需要修改launcher就行,在图标的点击事件处理中增加一点逻辑即可。这里就分析一下如何在第二种和第三种启动方式中实现禁用模式。

数据传递

哪个APP被禁用是APP设置的,所以禁用信息要从APP传递到framework中。这个信息不仅要能get到,而且要能实时的监听。要做到这种跨进程的数据传递,做好的方式就是通过ContentProvider。

NotificationManagerService

Android中发送通知的方法在NotificationManager中,所以我们从NotificationManager开始找代码。NotificationManager中的notify方法有如下三个

public void notify(int id, Notification notification)
{
    notify(null, id, notification);
}

public void notify(String tag, int id, Notification notification)
{
    notifyAsUser(tag, id, notification, new UserHandle(UserHandle.myUserId()));
}

public void notifyAsUser(String tag, int id, Notification notification, UserHandle user)
{
    INotificationManager service = getService();
    String pkg = mContext.getPackageName();
    // Fix the notification as best we can.
    Notification.addFieldsFromContext(mContext, notification);
    if (notification.sound != null) {
        notification.sound = notification.sound.getCanonicalUri();
        if (StrictMode.vmFileUriExposureEnabled()) {
            notification.sound.checkFileUriExposed("Notification.sound");
        }
    }
    fixLegacySmallIcon(notification, pkg);
    if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) {
        if (notification.getSmallIcon() == null) {
            throw new IllegalArgumentException("Invalid notification (no valid small icon): "
                    + notification);
        }
    }
    if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")");
    notification.reduceImageSizes(mContext);
    ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
    boolean isLowRam = am.isLowRamDevice();
    final Notification copy = Builder.maybeCloneStrippedForDelivery(notification, isLowRam);
    // 关键在这里
    try {
        service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,
                copy, user.getIdentifier());
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}

可以看到,这三个方法实际上最终都调用到了notifyAsUser方法中。这个方法的前面是一些参数的检查,关键的内容是下面这行:

service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id, copy, user.getIdentifier());

service的类型是INotificationManager接口,熟悉Framework的朋友都知道,这是一个aidl接口,其实现就是xxxService。这里就是NotificationManagerService。下面就看下NotificationManagerService的enqueueNotificationWithTag方法。

@Override
public void enqueueNotificationWithTag(String pkg, String opPkg, String tag, int id,
        Notification notification, int userId) throws RemoteException {
    enqueueNotificationInternal(pkg, opPkg, Binder.getCallingUid(),
            Binder.getCallingPid(), tag, id, notification, userId);
}

这里实际调用了enqueueNotificationInternal方法。这种xxxInternal的函数命名方式也是Android的常规操作了。

void enqueueNotificationInternal(final String pkg, final String opPkg, final int callingUid,
		final int callingPid, final String tag, final int id, final Notification notification,
		int incomingUserId) {
	if (DBG) {
		Slog.v(TAG, "enqueueNotificationInternal: pkg=" + pkg + " id=" + id
				+ " notification=" + notification);
	}
	checkCallerIsSystemOrSameApp(pkg);

	final int userId = ActivityManager.handleIncomingUser(callingPid,
			callingUid, incomingUserId, true, false, "enqueueNotification", pkg);
	final UserHandle user = new UserHandle(userId);

	if (pkg == null || notification == null) {
		throw new IllegalArgumentException("null not allowed: pkg=" + pkg
				+ " id=" + id + " notification=" + notification);
	}

	// The system can post notifications for any package, let us resolve that.
	final int notificationUid = resolveNotificationUid(opPkg, callingUid, userId);

	// Fix the notification as best we can.
	try {
		final ApplicationInfo ai = mPackageManagerClient.getApplicationInfoAsUser(
				pkg, PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
				(userId == UserHandle.USER_ALL) ? UserHandle.USER_SYSTEM : userId);
		Notification.addFieldsFromContext(ai, notification);

		int canColorize = mPackageManagerClient.checkPermission(
				android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, pkg);
		if (canColorize == PERMISSION_GRANTED) {
			notification.flags |= Notification.FLAG_CAN_COLORIZE;
		} else {
			notification.flags &= ~Notification.FLAG_CAN_COLORIZE;
		}

	} catch (NameNotFoundException e) {
		Slog.e(TAG, "Cannot create a context for sending app", e);
		return;
	}

	mUsageStats.registerEnqueuedByApp(pkg);

	// setup local book-keeping
	String channelId = notification.getChannelId();
	if (mIsTelevision && (new Notification.TvExtender(notification)).getChannelId() != null) {
		channelId = (new Notification.TvExtender(notification)).getChannelId();
	}
	final NotificationChannel channel = mRankingHelper.getNotificationChannel(pkg,
			notificationUid, channelId, false /* includeDeleted */);
	if (channel == null) {
		final String noChannelStr = "No Channel found for "
				+ "pkg=" + pkg
				+ ", channelId=" + channelId
				+ ", id=" + id
				+ ", tag=" + tag
				+ ", opPkg=" + opPkg
				+ ", callingUid=" + callingUid
				+ ", userId=" + userId
				+ ", incomingUserId=" + incomingUserId
				+ ", notificationUid=" + notificationUid
				+ ", notification=" + notification;
		Log.e(TAG, noChannelStr);
		doChannelWarningToast("Developer warning for package \"" + pkg + "\"\n" +
				"Failed to post notification on channel \"" + channelId + "\"\n" +
				"See log for more details");
		return;
	}

	final StatusBarNotification n = new StatusBarNotification(
			pkg, opPkg, id, tag, notificationUid, callingPid, notification,
			user, null, System.currentTimeMillis());
	final NotificationRecord r = new NotificationRecord(getContext(), n, channel);

	if ((notification.flags & Notification.FLAG_FOREGROUND_SERVICE) != 0
			&& (channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_IMPORTANCE) == 0
			&& (r.getImportance() == IMPORTANCE_MIN || r.getImportance() == IMPORTANCE_NONE)) {
		// Increase the importance of foreground service notifications unless the user had an
		// opinion otherwise
		if (TextUtils.isEmpty(channelId)
				|| NotificationChannel.DEFAULT_CHANNEL_ID.equals(channelId)) {
			r.setImportance(IMPORTANCE_LOW, "Bumped for foreground service");
		} else {
			channel.setImportance(IMPORTANCE_LOW);
			mRankingHelper.updateNotificationChannel(pkg, notificationUid, channel, false);
			r.updateNotificationChannel(channel);
		}
	}

	if (!checkDisqualifyingFeatures(userId, notificationUid, id, tag, r,
			r.sbn.getOverrideGroupKey() != null)) {
		return;
	}

	// Whitelist pending intents.
	if (notification.allPendingIntents != null) {
		final int intentCount = notification.allPendingIntents.size();
		if (intentCount > 0) {
			final ActivityManagerInternal am = LocalServices
					.getService(ActivityManagerInternal.class);
			final long duration = LocalServices.getService(
					DeviceIdleController.LocalService.class).getNotificationWhitelistDuration();
			for (int i = 0; i < intentCount; i++) {
				PendingIntent pendingIntent = notification.allPendingIntents.valueAt(i);
				if (pendingIntent != null) {
					am.setPendingIntentWhitelistDuration(pendingIntent.getTarget(),
							WHITELIST_TOKEN, duration);
				}
			}
		}
	}

	mHandler.post(new EnqueueNotificationRunnable(userId, r));
}

这个函数虽然很长,但是只要静下心来看一下,会发现有用的没几行(其实只有4行)。

if (DBG) {
	Slog.v(TAG, "enqueueNotificationInternal: pkg=" + pkg + " id=" + id
			+ " notification=" + notification);
}

函数最开头的这4行,给了我们重要的信息:包名。包名是直接从NotificationManager中传过来的,所以我们可以在这里对包名进行过滤,发现是被禁用掉的包名就直接返回。这个函数的返回值类型是void,直接返回也不会有其他的影响。
我们把过滤的逻辑就加在这个打印日志行的下面,实际上,就是在NotificationManagerService在处理应用发送通知的一开头就进行过滤,这样对其内部逻辑的影响最小,避免Service内部的状态被我们加的代码搞乱。

好的,改到这里,被禁用的APP就不能发送新的通知了。那么,在被禁用之前发送的通知怎么办呢?用户点击之后还是可以进入啊?所以,在通过ContentProvider监听到禁用APP的名单发生变化之后,要在onChange函数里面,把已经用的APP的所有通知全部清除掉。下面我们来看看NotificationManager中的清除通知的函数。

/**
 * Cancel a previously shown notification.  If it's transient, the view
 * will be hidden.  If it's persistent, it will be removed from the status
 * bar.
 */
public void cancel(int id)
{
    cancel(null, id);
}

/**
 * Cancel a previously shown notification.  If it's transient, the view
 * will be hidden.  If it's persistent, it will be removed from the status
 * bar.
 */
public void cancel(String tag, int id)
{
    cancelAsUser(tag, id, new UserHandle(UserHandle.myUserId()));
}

/**
 * @hide
 */
public void cancelAsUser(String tag, int id, UserHandle user)
{
    INotificationManager service = getService();
    String pkg = mContext.getPackageName();
    if (localLOGV) Log.v(TAG, pkg + ": cancel(" + id + ")");
    try {
        service.cancelNotificationWithTag(pkg, tag, id, user.getIdentifier());
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}

/**
 * Cancel all previously shown notifications. See {@link #cancel} for the
 * detailed behavior.
 */
public void cancelAll()
{
    INotificationManager service = getService();
    String pkg = mContext.getPackageName();
    if (localLOGV) Log.v(TAG, pkg + ": cancelAll()");
    try {
        service.cancelAllNotifications(pkg, UserHandle.myUserId());
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}

前2个函数最终都调用到了cancelAsUser,它和cancelAll的区别就是后者会把当前应用发的所有通知都清除掉。所以,看这个就好了。这里调用到了NotificationManagerService的cancelAllNotifications函数。

@Override
public void cancelAllNotifications(String pkg, int userId) {
    checkCallerIsSystemOrSameApp(pkg);

    userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(),
            Binder.getCallingUid(), userId, true, false, "cancelAllNotifications", pkg);

    // Calling from user space, don't allow the canceling of actively
    // running foreground services.
    cancelAllNotificationsInt(Binder.getCallingUid(), Binder.getCallingPid(),
            pkg, null, 0, Notification.FLAG_FOREGROUND_SERVICE, true, userId,
            REASON_APP_CANCEL_ALL, null);
}

这个函数就是通过cancelAllNotificationsInt这个函数,清除掉指定包名的所有通知。因为这个函数时实现的INotificationManager.Stub,所以,我们在NotificationObserver里面调用不到,所以,我们仿照这个函数的写法,调用cancelAllNotificationsInt函数。我们看一下cancelAllNotificationsInt函数的定义:

**
 * Cancels a notification ONLY if it has all of the {@code mustHaveFlags}
 * and none of the {@code mustNotHaveFlags}.
 */
void cancelNotification(final int callingUid, final int callingPid,
        final String pkg, final String tag, final int id,
        final int mustHaveFlags, final int mustNotHaveFlags, final boolean sendDelete,
        final int userId, final int reason, final ManagedServiceInfo listener) {
        ...
        }

这个函数里面有两个flag:mustHaveFlags和mustNotHaveFlags。从函数注释看出,这个函数清除的通知必须包含所有的mustHaveFlags,同时mustNotHaveFlag必须一个都没有。那么,对我们来说,我们要清除的是指定应用的所有通知,不要这么多的限制条件。所以,这两个flag传0即可。

cancelAllNotificationsInt(Binder.getCallingUid(),
                            Binder.getCallingPid(),
                            pkg,
                            null,
                            0,
                            0,
                            true,
                            UserHandle.myUserId(),
                            REASON_APP_CANCEL_ALL,
                            null);
  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值