content: /*
   * Copyright (C) 2016 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  package com.android.launcher3.model;
  
  import android.content.ComponentName;
  import android.content.Context;
  import android.content.Intent;
  import android.graphics.Bitmap;
  import android.os.Process;
  import android.os.UserHandle;
  import android.util.ArrayMap;
  import android.util.Log;
  
  import com.android.launcher3.AllAppsList;
  import com.android.launcher3.AppInfo;
  import com.android.launcher3.IconCache;
  import com.android.launcher3.InstallShortcutReceiver;
  import com.android.launcher3.ItemInfo;
  import com.android.launcher3.LauncherAppState;
  import com.android.launcher3.LauncherAppWidgetInfo;
  import com.android.launcher3.LauncherModel.CallbackTask;
  import com.android.launcher3.LauncherModel.Callbacks;
  import com.android.launcher3.LauncherSettings.Favorites;
  import com.android.launcher3.SessionCommitReceiver;
  import com.android.launcher3.ShortcutInfo;
  import com.android.launcher3.Utilities;
  import com.android.launcher3.compat.LauncherAppsCompat;
  import com.android.launcher3.compat.UserManagerCompat;
  import com.android.launcher3.config.FeatureFlags;
  import com.android.launcher3.graphics.LauncherIcons;
  import com.android.launcher3.util.FlagOp;
  import com.android.launcher3.util.ItemInfoMatcher;
  import com.android.launcher3.util.LongArrayMap;
  import com.android.launcher3.util.PackageManagerHelper;
  import com.android.launcher3.util.PackageUserKey;
  
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.HashSet;
  
  /**
   * Handles updates due to changes in package manager (app installed/updated/removed)
   * or when a user availability changes.
   */
  public class PackageUpdatedTask extends BaseModelUpdateTask {
  
      private static final boolean DEBUG = false;
      private static final String TAG = "PackageUpdatedTask";
  
      public static final int OP_NONE = 0;
      public static final int OP_ADD = 1;
      public static final int OP_UPDATE = 2;
      public static final int OP_REMOVE = 3; // uninstalled
      public static final int OP_UNAVAILABLE = 4; // external media unmounted
      public static final int OP_SUSPEND = 5; // package suspended
      public static final int OP_UNSUSPEND = 6; // package unsuspended
      public static final int OP_USER_AVAILABILITY_CHANGE = 7; // user available/unavailable
      public static final int OP_RELOAD = 8; // clears cache
  
      private final int mOp;
      private final UserHandle mUser;
      private final String[] mPackages;
  
      public PackageUpdatedTask(int op, UserHandle user, String... packages) {
          mOp = op;
          mUser = user;
          mPackages = packages;
      }
  
      @Override
      public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList appsList) {
          final Context context = app.getContext();
          final IconCache iconCache = app.getIconCache();
  
          final String[] packages = mPackages;
          final int N = packages.length;
          FlagOp flagOp = FlagOp.NO_OP;
          final HashSet<String> packageSet = new HashSet<>(Arrays.asList(packages));
          ItemInfoMatcher matcher = ItemInfoMatcher.ofPackages(packageSet, mUser);
          switch (mOp) {
              case OP_ADD: {
                  for (int i = 0; i < N; i++) {
                      if (DEBUG) Log.d(TAG, "mAllAppsList.addPackage " + packages[i]);
                      iconCache.updateIconsForPkg(packages[i], mUser);
                      if (FeatureFlags.LAUNCHER3_PROMISE_APPS_IN_ALL_APPS) {
                          appsList.removePackage(packages[i], Process.myUserHandle());
                      }
                      appsList.addPackage(context, packages[i], mUser);
  
                      // Automatically add homescreen icon for work profile apps for below O device.
                      if (!Utilities.ATLEAST_OREO && !Process.myUserHandle().equals(mUser)) {
                          SessionCommitReceiver.queueAppIconAddition(context, packages[i], mUser);
                      }
                  }
                  flagOp = FlagOp.removeFlag(ShortcutInfo.FLAG_DISABLED_NOT_AVAILABLE);
                  break;
              }
              case OP_UPDATE:
                  for (int i = 0; i < N; i++) {
                      if (DEBUG) Log.d(TAG, "mAllAppsList.updatePackage " + packages[i]);
                      iconCache.updateIconsForPkg(packages[i], mUser);
                      appsList.updatePackage(context, packages[i], mUser);
                      app.getWidgetCache().removePackage(packages[i], mUser);
                  }
                  // Since package was just updated, the target must be available now.
                  flagOp = FlagOp.removeFlag(ShortcutInfo.FLAG_DISABLED_NOT_AVAILABLE);
                  break;
              case OP_REMOVE: {
                  for (int i = 0; i < N; i++) {
                      iconCache.removeIconsForPkg(packages[i], mUser);
                  }
                  // Fall through
              }
              case OP_UNAVAILABLE:
                  for (int i = 0; i < N; i++) {
                      if (DEBUG) Log.d(TAG, "mAllAppsList.removePackage " + packages[i]);
                      appsList.removePackage(packages[i], mUser);
                      app.getWidgetCache().removePackage(packages[i], mUser);
                  }
                  flagOp = FlagOp.addFlag(ShortcutInfo.FLAG_DISABLED_NOT_AVAILABLE);
                  break;
              case OP_SUSPEND:
              case OP_UNSUSPEND:
                  flagOp = mOp == OP_SUSPEND ?
                          FlagOp.addFlag(ShortcutInfo.FLAG_DISABLED_SUSPENDED) :
                          FlagOp.removeFlag(ShortcutInfo.FLAG_DISABLED_SUSPENDED);
                  if (DEBUG) Log.d(TAG, "mAllAppsList.(un)suspend " + N);
                  appsList.updateDisabledFlags(matcher, flagOp);
                  break;
              case OP_USER_AVAILABILITY_CHANGE:
                  flagOp = UserManagerCompat.getInstance(context).isQuietModeEnabled(mUser)
                          ? FlagOp.addFlag(ShortcutInfo.FLAG_DISABLED_QUIET_USER)
                          : FlagOp.removeFlag(ShortcutInfo.FLAG_DISABLED_QUIET_USER);
                  // We want to update all packages for this user.
                  matcher = ItemInfoMatcher.ofUser(mUser);
                  appsList.updateDisabledFlags(matcher, flagOp);
                  break;
              case OP_RELOAD:
                  if (DEBUG) Log.d(TAG, "mAllAppsList.reloadPackages");
                  appsList.reloadPackages(context, mUser);
                  break;
          }
  
          final ArrayList<AppInfo> addedOrModified = new ArrayList<>();
          addedOrModified.addAll(appsList.added);
          appsList.added.clear();
          addedOrModified.addAll(appsList.modified);
          appsList.modified.clear();
  
          final ArrayList<AppInfo> removedApps = new ArrayList<>(appsList.removed);
          appsList.removed.clear();
  
          final ArrayMap<ComponentName, AppInfo> addedOrUpdatedApps = new ArrayMap<>();
          if (!addedOrModified.isEmpty() || mOp == OP_UPDATE) {
              scheduleCallbackTask(new CallbackTask() {
                  @Override
                  public void execute(Callbacks callbacks) {
                      callbacks.bindAppsAddedOrUpdated(addedOrModified);
                  }
              });
              for (AppInfo ai : addedOrModified) {
                  addedOrUpdatedApps.put(ai.componentName, ai);
              }
          }
  
          final LongArrayMap<Boolean> removedShortcuts = new LongArrayMap<>();
  
          // Update shortcut infos
          if (mOp == OP_ADD || flagOp != FlagOp.NO_OP) {
              final ArrayList<ShortcutInfo> updatedShortcuts = new ArrayList<>();
              final ArrayList<LauncherAppWidgetInfo> widgets = new ArrayList<>();
  
              // For system apps, package manager send OP_UPDATE when an app is enabled.
              final boolean isNewApkAvailable = mOp == OP_ADD || mOp == OP_UPDATE;
              synchronized (dataModel) {
                  for (ItemInfo info : dataModel.itemsIdMap) {
                      if (info instanceof ShortcutInfo && mUser.equals(info.user)) {
                          ShortcutInfo si = (ShortcutInfo) info;
                          boolean infoUpdated = false;
                          boolean shortcutUpdated = false;
  
                          // Update shortcuts which use iconResource.
                          if ((si.iconResource != null)
                                  && packageSet.contains(si.iconResource.packageName)) {
                              Bitmap icon = LauncherIcons.createIconBitmap(si.iconResource, context);
                              if (icon != null) {
                                  si.iconBitmap = icon;
                                  infoUpdated = true;
                              }
                          }
  
                          ComponentName cn = si.getTargetComponent();
                          if (cn != null && matcher.matches(si, cn)) {
                              AppInfo appInfo = addedOrUpdatedApps.get(cn);
  
                              if (si.hasStatusFlag(ShortcutInfo.FLAG_SUPPORTS_WEB_UI)) {
                                  removedShortcuts.put(si.id, false);
                                  if (mOp == OP_REMOVE) {
                                      continue;
                                  }
                              }
  
                              if (si.isPromise() && isNewApkAvailable) {
                                  if (si.hasStatusFlag(ShortcutInfo.FLAG_AUTOINSTALL_ICON)) {
                                      // Auto install icon
                                      LauncherAppsCompat launcherApps
                                              = LauncherAppsCompat.getInstance(context);
                                      if (!launcherApps.isActivityEnabledForProfile(cn, mUser)) {
                                          // Try to find the best match activity.
                                          Intent intent = new PackageManagerHelper(context)
                                                  .getAppLaunchIntent(cn.getPackageName(), mUser);
                                          if (intent != null) {
                                              cn = intent.getComponent();
                                              appInfo = addedOrUpdatedApps.get(cn);
                                          }
  
                                          if (intent != null && appInfo != null) {
                                              si.intent = intent;
                                              si.status = ShortcutInfo.DEFAULT;
                                              infoUpdated = true;
                                          } else if (si.hasPromiseIconUi()) {
                                              removedShortcuts.put(si.id, true);
                                              continue;
                                          }
                                      }
                                  } else {
                                      si.status = ShortcutInfo.DEFAULT;
                                      infoUpdated = true;
                                  }
                              }
  
                              if (isNewApkAvailable &&
                                      si.itemType == Favorites.ITEM_TYPE_APPLICATION) {
                                  iconCache.getTitleAndIcon(si, si.usingLowResIcon);
                                  infoUpdated = true;
                              }
  
                              int oldDisabledFlags = si.isDisabled;
                              si.isDisabled = flagOp.apply(si.isDisabled);
                              if (si.isDisabled != oldDisabledFlags) {
                                  shortcutUpdated = true;
                              }
                          }
  
                          if (infoUpdated || shortcutUpdated) {
                              updatedShortcuts.add(si);
                          }
                          if (infoUpdated) {
                              getModelWriter().updateItemInDatabase(si);
                          }
                      } else if (info instanceof LauncherAppWidgetInfo && isNewApkAvailable) {
                          LauncherAppWidgetInfo widgetInfo = (LauncherAppWidgetInfo) info;
                          if (mUser.equals(widgetInfo.user)
                                  && widgetInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)
                                  && packageSet.contains(widgetInfo.providerName.getPackageName())) {
                              widgetInfo.restoreStatus &=
                                      ~LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY &
                                              ~LauncherAppWidgetInfo.FLAG_RESTORE_STARTED;
  
                              // adding this flag ensures that launcher shows 'click to setup'
                              // if the widget has a config activity. In case there is no config
                              // activity, it will be marked as 'restored' during bind.
                              widgetInfo.restoreStatus |= LauncherAppWidgetInfo.FLAG_UI_NOT_READY;
  
                              widgets.add(widgetInfo);
                              getModelWriter().updateItemInDatabase(widgetInfo);
                          }
                      }
                  }
              }
  
              bindUpdatedShortcuts(updatedShortcuts, mUser);
              if (!removedShortcuts.isEmpty()) {
                  deleteAndBindComponentsRemoved(ItemInfoMatcher.ofItemIds(removedShortcuts, false));
              }
  
              if (!widgets.isEmpty()) {
                  scheduleCallbackTask(new CallbackTask() {
                      @Override
                      public void execute(Callbacks callbacks) {
                          callbacks.bindWidgetsRestored(widgets);
                      }
                  });
              }
          }
  
          final HashSet<String> removedPackages = new HashSet<>();
          final HashSet<ComponentName> removedComponents = new HashSet<>();
          if (mOp == OP_REMOVE) {
              // Mark all packages in the broadcast to be removed
              Collections.addAll(removedPackages, packages);
  
              // No need to update the removedComponents as
              // removedPackages is a super-set of removedComponents
          } else if (mOp == OP_UPDATE) {
              // Mark disabled packages in the broadcast to be removed
              final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(context);
              for (int i=0; i<N; i++) {
                  if (!launcherApps.isPackageEnabledForProfile(packages[i], mUser)) {
                      removedPackages.add(packages[i]);
                  }
              }
  
              // Update removedComponents as some components can get removed during package update
              for (AppInfo info : removedApps) {
                  removedComponents.add(info.componentName);
              }
          }
  
          if (!removedPackages.isEmpty() || !removedComponents.isEmpty()) {
              ItemInfoMatcher removeMatch = ItemInfoMatcher.ofPackages(removedPackages, mUser)
                      .or(ItemInfoMatcher.ofComponents(removedComponents, mUser))
                      .and(ItemInfoMatcher.ofItemIds(removedShortcuts, true));
              deleteAndBindComponentsRemoved(removeMatch);
  
              // Remove any queued items from the install queue
              InstallShortcutReceiver.removeFromInstallQueue(context, removedPackages, mUser);
          }
  
          if (!removedApps.isEmpty()) {
              // Remove corresponding apps from All-Apps
              scheduleCallbackTask(new CallbackTask() {
                  @Override
                  public void execute(Callbacks callbacks) {
                      callbacks.bindAppInfosRemoved(removedApps);
                  }
              });
          }
  
          // Notify launcher of widget update. From marshmallow onwards we use AppWidgetHost to
          // get widget update signals.
          if (!Utilities.ATLEAST_MARSHMALLOW &&
                  (mOp == OP_ADD || mOp == OP_REMOVE || mOp == OP_UPDATE)) {
              scheduleCallbackTask(new CallbackTask() {
                  @Override
                  public void execute(Callbacks callbacks) {
                      callbacks.notifyWidgetProvidersChanged();
                  }
              });
          } else if (Utilities.ATLEAST_OREO && mOp == OP_ADD) {
              // Load widgets for the new package. Changes due to app updates are handled through
              // AppWidgetHost events, this is just to initialize the long-press options.
              for (int i = 0; i < N; i++) {
                  dataModel.widgetsModel.update(app, new PackageUserKey(packages[i], mUser));
              }
              bindUpdatedWidgets(dataModel);
          }
      }
  }
  