// Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.chrome.browser.incognito; import android.annotation.TargetApi; import android.app.Activity; import android.app.ActivityManager; import android.app.ActivityManager.AppTask; import android.app.ActivityManager.RecentTaskInfo; import android.app.IntentService; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Build; import android.text.TextUtils; import android.util.Pair; import org.chromium.base.ActivityState; import org.chromium.base.ApplicationStatus; import org.chromium.base.ContextUtils; import org.chromium.base.ThreadUtils; import org.chromium.base.VisibleForTesting; import org.chromium.chrome.browser.ChromeTabbedActivity; import org.chromium.chrome.browser.TabState; import org.chromium.chrome.browser.document.ChromeLauncherActivity; import org.chromium.chrome.browser.document.DocumentUtils; import org.chromium.chrome.browser.tabmodel.TabWindowManager; import org.chromium.chrome.browser.tabmodel.TabbedModeTabPersistencePolicy; import java.io.File; import java.lang.ref.WeakReference; import java.util.HashSet; import java.util.List; import java.util.Set; /** * Service that handles the action of clicking on the incognito notification. */ public class IncognitoNotificationService extends IntentService { private static final String TAG = "incognito_notification"; private static final String ACTION_CLOSE_ALL_INCOGNITO = "com.google.android.apps.chrome.incognito.CLOSE_ALL_INCOGNITO"; @VisibleForTesting public static PendingIntent getRemoveAllIncognitoTabsIntent(Context context) { Intent intent = new Intent(context, IncognitoNotificationService.class); intent.setAction(ACTION_CLOSE_ALL_INCOGNITO); return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_ONE_SHOT); } /** Empty public constructor needed by Android. */ public IncognitoNotificationService() { super(TAG); } @Override protected void onHandleIntent(Intent intent) { closeIncognitoTabsInRunningTabbedActivities(); boolean clearedIncognito = deleteIncognitoStateFilesInDirectory( TabbedModeTabPersistencePolicy.getOrCreateTabbedModeStateDirectory()); // If we failed clearing all of the incognito tabs, then do not dismiss the notification. if (!clearedIncognito) return; ThreadUtils.runOnUiThreadBlocking(new Runnable() { @Override public void run() { int incognitoCount = TabWindowManager.getInstance().getIncognitoTabCount(); assert incognitoCount == 0; if (incognitoCount == 0) { IncognitoNotificationManager.dismissIncognitoNotification(); } } }); ThreadUtils.runOnUiThreadBlocking(new Runnable() { @Override public void run() { // Now ensure that the snapshots in recents are all cleared for Tabbed activities // to remove any trace of incognito mode. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { focusChromeIfNecessary(); } else { removeNonVisibleChromeTabbedRecentEntries(); } } }); } private void focusChromeIfNecessary() { Set<Integer> visibleTaskIds = getTaskIdsForVisibleActivities(); int tabbedTaskId = -1; List<WeakReference<Activity>> runningActivities = ApplicationStatus.getRunningActivities(); for (int i = 0; i < runningActivities.size(); i++) { Activity activity = runningActivities.get(i).get(); if (activity == null) continue; if (activity instanceof ChromeTabbedActivity) { tabbedTaskId = activity.getTaskId(); break; } } // If the task containing the tabbed activity is visible, then do nothing as there is no // snapshot that would need to be regenerated. if (visibleTaskIds.contains(tabbedTaskId)) return; Context context = ContextUtils.getApplicationContext(); Intent startIntent = new Intent(Intent.ACTION_MAIN); startIntent.setPackage(context.getPackageName()); startIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(startIntent); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) private void removeNonVisibleChromeTabbedRecentEntries() { Set<Integer> visibleTaskIds = getTaskIdsForVisibleActivities(); Context context = ContextUtils.getApplicationContext(); ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); PackageManager pm = getPackageManager(); for (AppTask task : manager.getAppTasks()) { RecentTaskInfo info = DocumentUtils.getTaskInfoFromTask(task); if (info == null) continue; String className = DocumentUtils.getTaskClassName(task, pm); // It is not easily possible to distinguish between tasks sitting on top of // ChromeLauncherActivity, so we treat them all as likely ChromeTabbedActivities and // close them to be on the cautious side of things. if ((TextUtils.equals(className, ChromeTabbedActivity.class.getName()) || TextUtils.equals(className, ChromeLauncherActivity.class.getName())) && !visibleTaskIds.contains(info.id)) { task.finishAndRemoveTask(); } } } private Set<Integer> getTaskIdsForVisibleActivities() { List<WeakReference<Activity>> runningActivities = ApplicationStatus.getRunningActivities(); Set<Integer> visibleTaskIds = new HashSet<>(); for (int i = 0; i < runningActivities.size(); i++) { Activity activity = runningActivities.get(i).get(); if (activity == null) continue; int activityState = ApplicationStatus.getStateForActivity(activity); if (activityState != ActivityState.STOPPED && activityState != ActivityState.DESTROYED) { visibleTaskIds.add(activity.getTaskId()); } } return visibleTaskIds; } /** * Iterate across the running activities and for any running tabbed mode activities close their * incognito tabs. * * @see TabWindowManager#getIndexForWindow(Activity) */ private void closeIncognitoTabsInRunningTabbedActivities() { ThreadUtils.runOnUiThreadBlocking(new Runnable() { @Override public void run() { List<WeakReference<Activity>> runningActivities = ApplicationStatus.getRunningActivities(); for (int i = 0; i < runningActivities.size(); i++) { Activity activity = runningActivities.get(i).get(); if (activity == null) continue; if (!(activity instanceof ChromeTabbedActivity)) continue; ChromeTabbedActivity tabbedActivity = (ChromeTabbedActivity) activity; if (tabbedActivity.isActivityDestroyed()) continue; tabbedActivity.getTabModelSelector().getModel(true).closeAllTabs( false, false); } } }); } /** * @return Whether deleting all the incognito files was successful. */ private boolean deleteIncognitoStateFilesInDirectory(File directory) { File[] allTabStates = directory.listFiles(); if (allTabStates == null) return true; boolean deletionSuccessful = true; for (int i = 0; i < allTabStates.length; i++) { String fileName = allTabStates[i].getName(); Pair<Integer, Boolean> tabInfo = TabState.parseInfoFromFilename(fileName); if (tabInfo == null || !tabInfo.second) continue; deletionSuccessful &= allTabStates[i].delete(); } return deletionSuccessful; } }