Android8.0でAPP無効化モードを実現(一)


需要
プロダクトマネージャは、Androidタブレットでアプリケーションの無効化モードを実現することを要求します.インストール済みのアプリケーションが無効に設定されている場合、イニシエータではAPPアイコンが灰色で、APPが起動できません.
需要分析
アプリを開く方法は3つあります.1、イニシエータからアイコンをクリックして起動します.2、APPのポップアップ通知をクリックして起動する.3、マルチタスクキーをクリックして、APPを選択する.この3つの起動方式のうち、1つ目は無効化が容易で、launcherを修正するだけでいいので、アイコンのクリックイベント処理に論理を少し増やせばいいです.ここでは、2番目と3番目の起動方式で無効モードを実現する方法を分析します.
データ転送
どのアプリが無効になっているかはアプリ設定なので、無効情報はアプリからframeworkに渡さなければなりません.この情報はgetできるだけでなく、リアルタイムで傍受できるようにしなければならない.このようなプロセス間のデータ転送を行うには、ContentProviderを通じて行うことができます.
NotificationManagerService
Androidで通知を送る方法はNotificationManagerにあるので、NotificationManagerからコードを探します.Notifyメソッドは、次の3つです.
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();
    }
}

この3つの方法は実際にはnotifyAsUserメソッドに呼び出されていることがわかります.この方法の前にはいくつかのパラメータのチェックがあり、重要な内容は次の行です.
service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id, copy, user.getIdentifier());

サービスのタイプは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 + "\"
"
+ "Failed to post notification on channel \"" + channelId + "\"
"
+ "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がアプリケーションの送信通知を処理する最初からフィルタリングを行い、内部ロジックへの影響を最小限に抑え、サービス内部の状態が私たちが追加したコードに混乱しないようにします.
はい、ここに変更すると、無効になっているアプリは新しい通知を送信できません.では、無効になる前に送信された通知はどうすればいいのでしょうか.ユーザーがクリックしてもアクセスできますか?そのため、ContentProviderでアプリを無効にしたリストが変化したことを傍受した後、onChange関数ですでに使用しているアプリのすべての通知をクリアします.次に、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という関数によって、指定したパケット名のすべての通知を消去します.この関数の場合に実装されるINotificationManagement.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が2つあります.mustHaveFlagsとmustNotHaveFlagsです.関数注釈から、この関数クリアの通知には、すべてのmustHaveFlagsが含まれている必要があり、mustNotHaveFlagは1つも含まれていない必要があります.では、私たちにとって、私たちがクリアしなければならないのは、指定されたアプリケーションのすべての通知であり、こんなに多くの制限条件は必要ありません.だから、この2つのflagは0を伝えればいいです.
cancelAllNotificationsInt(Binder.getCallingUid(),
                            Binder.getCallingPid(),
                            pkg,
                            null,
                            0,
                            0,
                            true,
                            UserHandle.myUserId(),
                            REASON_APP_CANCEL_ALL,
                            null);