Android8.0에서 APP 비활성화 모드(一)

요구 사항
제품 관리자는 안드로이드 태블릿에서 응용 프로그램의 비활성화 모드를 구현할 것을 요구합니다.설치된 애플리케이션이 비활성화로 설정되면 이니시에이터에서 APP 아이콘이 회색이고 APP가 시작되지 않습니다.
수요 분석
하나의 앱을 여는 방식은 세 가지가 있다. 첫째, 시동기에서 아이콘을 클릭하여 시작한다.2. 앱에서 팝업된 알림을 클릭하여 시작합니다.3. 멀티퀘스트 버튼을 클릭하고 앱을 선택한다.이 세 가지 시작 방식 중 첫 번째는 비활성화되기 쉬우므로 launcher를 수정하면 되고 아이콘의 클릭 이벤트 처리에 논리를 추가하면 된다.두 번째와 세 번째 시작 방식에서 어떻게 금지 모드를 실현하는지 분석해 보겠습니다.
데이터 전달
어떤 앱이 비활성화되는지는 앱이 설정한 것이기 때문에 비활성화 정보는 앱에서 프레임워크로 전달해야 한다.이 정보는 get이 도착해야 할 뿐만 아니라 실시간으로 감청해야 한다.이런 크로스 프로세스의 데이터 전달을 하려면, 좋은 방법은Content Provider를 통해 하는 것이다.
NotificationManagerService
Android에서 알림을 보내는 방법은 Notification Manager에 있기 때문에 Notification Manager에서 코드를 찾기 시작합니다.Notification Manager의 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();
    }
}

이 세 가지 방법이 실제로는 notify Asuser 방법으로 호출되었다는 것을 알 수 있다.이 방법의 앞에는 몇 가지 파라미터의 검사가 있는데, 관건적인 내용은 다음과 같다.
service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id, copy, user.getIdentifier());

서비스의 유형은 INotification Manager 인터페이스입니다. Framework에 익숙한 친구들은 모두 알고 있습니다. 이것은 aidl 인터페이스이고 그 실현은 xxx 서비스입니다.Notification Manager Service입니다.Notification Manager Service의 enqueue Notification WithTag 방법을 살펴보겠습니다.
@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);
}

여기에는 enqueue Notification 인터넷 방법이 실제로 호출되었다.이런 xxx인터넷의 함수 명명 방식도 안드로이드의 일반적인 조작이다.
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);
}

함수의 맨 처음 네 줄은 우리에게 중요한 정보인 가방 이름을 주었다.패키지 이름은 Notification Manager에서 직접 전달되기 때문에 패키지 이름을 필터할 수 있습니다. 비활성화된 패키지 이름을 발견하면 바로 되돌아옵니다.이 함수의 반환값 형식은void이며, 직접 반환해도 다른 영향을 주지 않습니다.우리는 필터의 논리를 이 인쇄 로그 줄 아래에 추가합니다. 실제적으로Notification Manager 서비스는 처리 응용 프로그램이 알림을 보내는 첫머리에 필터를 해서 내부 논리에 대한 영향을 최소화하고 서비스 내부의 상태가 우리가 추가한 코드에 의해 흐트러지지 않도록 합니다.
네, 여기로 변경하면 비활성화된 앱은 새로운 알림을 보낼 수 없습니다.그럼 비활성화되기 전에 보내는 알림은 어떻게 해야 하나요?사용자가 클릭한 후에도 들어갈 수 있습니까?따라서 Content Provider를 통해 앱을 사용하지 않는 명단의 변화를 감청한 후 onChange 함수에서 이미 사용한 앱에 대한 모든 알림을 삭제해야 한다.Notification Manager에서 알림을 지우는 함수를 살펴보겠습니다.
/**
 * 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();
    }
}

앞의 두 함수는 최종적으로 cancelAsuser에 호출되었는데, 이것은 cancelAll과 다르다. 후자는 현재 응용 프로그램이 보낸 모든 알림을 삭제할 것이다.그러니까 이거 보면 돼.Notification Manager Service의 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, 그래서, 우리는 Notification Observer에서 호출할 수 없습니다. 따라서, 우리는 이 함수의 쓰기 방식을 본떠서 cancelAll Notifications Int 함수를 호출합니다.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);

좋은 웹페이지 즐겨찾기