Merge "P2P App Sharing: Add Shareability Cache"
diff --git a/Android.bp b/Android.bp
index c8d9186..ea94f87 100644
--- a/Android.bp
+++ b/Android.bp
@@ -246,7 +246,9 @@
     static_libs: [
         "Launcher3CommonDepsLib",
         "QuickstepResLib",
+        "androidx.room_room-runtime",
     ],
+    plugins: ["androidx.room_room-compiler-plugin"],
     manifest: "quickstep/AndroidManifest-launcher.xml",
     additional_manifests: [
         "go/AndroidManifest.xml",
diff --git a/Android.mk b/Android.mk
index c1dbc53..ceaaf13 100644
--- a/Android.mk
+++ b/Android.mk
@@ -105,7 +105,7 @@
   LOCAL_SDK_VERSION := system_current
   LOCAL_MIN_SDK_VERSION := 26
 endif
-LOCAL_STATIC_ANDROID_LIBRARIES := Launcher3CommonDepsLib
+LOCAL_STATIC_ANDROID_LIBRARIES := LauncherGoResLib
 
 LOCAL_SRC_FILES := \
     $(call all-java-files-under, src) \
diff --git a/go/AndroidManifest.xml b/go/AndroidManifest.xml
index 2671604..728b326 100644
--- a/go/AndroidManifest.xml
+++ b/go/AndroidManifest.xml
@@ -54,6 +54,10 @@
             android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
             android:enabled="false"
             tools:node="replace" />
+
+        <service
+            android:name="com.android.launcher3.model.AppShareabilityJobService"
+            android:permission="android.permission.BIND_JOB_SERVICE" />
     </application>
 
 </manifest>
diff --git a/go/quickstep/res/values/integers.xml b/go/quickstep/res/values/integers.xml
new file mode 100644
index 0000000..e6e8111
--- /dev/null
+++ b/go/quickstep/res/values/integers.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2021 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.
+  -->
+<resources>
+    <!-- Job IDs must be integers unique within their package -->
+    <integer name="app_shareability_job_id">200</integer>
+</resources>
\ No newline at end of file
diff --git a/go/quickstep/src/com/android/launcher3/AppSharing.java b/go/quickstep/src/com/android/launcher3/AppSharing.java
index b72e71c..fd5456c 100644
--- a/go/quickstep/src/com/android/launcher3/AppSharing.java
+++ b/go/quickstep/src/com/android/launcher3/AppSharing.java
@@ -28,7 +28,12 @@
 
 import androidx.core.content.FileProvider;
 
+import com.android.launcher3.model.AppShareabilityChecker;
+import com.android.launcher3.model.AppShareabilityJobService;
+import com.android.launcher3.model.AppShareabilityManager;
+import com.android.launcher3.model.AppShareabilityManager.ShareabilityStatus;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.popup.PopupDataProvider;
 import com.android.launcher3.popup.SystemShortcut;
 
 import java.io.File;
@@ -44,6 +49,11 @@
      * because it is unique to Go and not toggleable at runtime.
      */
     public static final boolean ENABLE_APP_SHARING = true;
+    /**
+     * With this flag enabled, the Share App button will be dynamically enabled/disabled based
+     * on each app's shareability status.
+     */
+    public static final boolean ENABLE_SHAREABILITY_CHECK = false;
 
     private static final String TAG = "AppSharing";
     private static final String FILE_PROVIDER_SUFFIX = ".overview.fileprovider";
@@ -51,22 +61,12 @@
     private static final String APP_MIME_TYPE = "application/application";
 
     private final String mSharingComponent;
+    private AppShareabilityManager mShareabilityMgr;
 
     private AppSharing(Launcher launcher) {
         mSharingComponent = launcher.getText(R.string.app_sharing_component).toString();
     }
 
-    private boolean canShare(ItemInfo info) {
-        /**
-         * TODO: Implement once b/168831749 has been resolved
-         * The implementation should check the validity of the app.
-         * It should also check whether the app is free or paid, returning false in the latter case.
-         * For now, all checks occur in the sharing app.
-         * So, we simply check whether the sharing app is defined.
-         */
-        return !TextUtils.isEmpty(mSharingComponent);
-    }
-
     private Uri getShareableUri(Context context, String path, String displayName) {
         String authority = BuildConfig.APPLICATION_ID + FILE_PROVIDER_SUFFIX;
         File pathFile = new File(path);
@@ -74,19 +74,39 @@
     }
 
     private SystemShortcut<Launcher> getShortcut(Launcher launcher, ItemInfo info) {
-        if (!canShare(info)) {
+        if (TextUtils.isEmpty(mSharingComponent)) {
             return null;
         }
-
         return new Share(launcher, info);
     }
 
     /**
+     * Instantiates AppShareabilityManager, which then reads app shareability data from disk
+     * Also schedules a job to update those data
+     * @param context The application context
+     * @param checker An implementation of AppShareabilityChecker to perform the actual checks
+     *                when updating the data
+     */
+    public static void setUpShareabilityCache(Context context, AppShareabilityChecker checker) {
+        AppShareabilityManager shareMgr = AppShareabilityManager.INSTANCE.get(context);
+        shareMgr.setShareabilityChecker(checker);
+        AppShareabilityJobService.schedule(context);
+    }
+
+    /**
      * The Share App system shortcut, used to initiate p2p sharing of a given app
      */
     public final class Share extends SystemShortcut<Launcher> {
+        private PopupDataProvider mPopupDataProvider;
+
         public Share(Launcher target, ItemInfo itemInfo) {
             super(R.drawable.ic_share, R.string.app_share_drop_target_label, target, itemInfo);
+            mPopupDataProvider = target.getPopupDataProvider();
+
+            if (ENABLE_SHAREABILITY_CHECK) {
+                mShareabilityMgr = AppShareabilityManager.INSTANCE.get(target);
+                checkShareability(/* requestUpdateIfUnknown */ true);
+            }
         }
 
         @Override
@@ -122,6 +142,27 @@
 
             AbstractFloatingView.closeAllOpenViews(mTarget);
         }
+
+        private void onStatusUpdated(boolean success) {
+            if (!success) {
+                // Something went wrong. Specific error logged in AppShareabilityManager.
+                return;
+            }
+            checkShareability(/* requestUpdateIfUnknown */ false);
+            mTarget.runOnUiThread(() -> {
+                mPopupDataProvider.redrawSystemShortcuts();
+            });
+        }
+
+        private void checkShareability(boolean requestUpdateIfUnknown) {
+            String packageName = mItemInfo.getTargetComponent().getPackageName();
+            @ShareabilityStatus int status = mShareabilityMgr.getStatus(packageName);
+            setEnabled(status == ShareabilityStatus.SHAREABLE);
+
+            if (requestUpdateIfUnknown && status == ShareabilityStatus.UNKNOWN) {
+                mShareabilityMgr.requestAppStatusUpdate(packageName, this::onStatusUpdated);
+            }
+        }
     }
 
     /**
diff --git a/go/quickstep/src/com/android/launcher3/model/AppShareabilityChecker.java b/go/quickstep/src/com/android/launcher3/model/AppShareabilityChecker.java
new file mode 100644
index 0000000..0a82904
--- /dev/null
+++ b/go/quickstep/src/com/android/launcher3/model/AppShareabilityChecker.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2021 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 androidx.annotation.Nullable;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * Interface for checking apps' shareability. Implementations need to be able to determine whether
+ * apps are shareable given their package names.
+ */
+public interface AppShareabilityChecker {
+    /**
+     * Checks the shareability of the provided apps. Once the check is complete, updates the
+     * provided manager with the results and calls the (optionally) provided callback.
+     * @param packageNames The apps to check
+     * @param shareMgr The manager to receive the results
+     * @param callback Optional callback to be invoked when the check is finished
+     */
+    void checkApps(List<String> packageNames, AppShareabilityManager shareMgr,
+            @Nullable Consumer<Boolean> callback);
+}
diff --git a/go/quickstep/src/com/android/launcher3/model/AppShareabilityDatabase.java b/go/quickstep/src/com/android/launcher3/model/AppShareabilityDatabase.java
new file mode 100644
index 0000000..03eed7e
--- /dev/null
+++ b/go/quickstep/src/com/android/launcher3/model/AppShareabilityDatabase.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2021 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 androidx.room.Dao;
+import androidx.room.Database;
+import androidx.room.Insert;
+import androidx.room.OnConflictStrategy;
+import androidx.room.Query;
+import androidx.room.RoomDatabase;
+import androidx.room.Update;
+
+import java.util.List;
+
+/**
+ * This database maintains a collection of AppShareabilityStatus items
+ * In its intended use case, there will be one entry for each app installed on the device
+ */
+@Database(entities = {AppShareabilityStatus.class}, exportSchema = false, version = 1)
+public abstract class AppShareabilityDatabase extends RoomDatabase {
+    /**
+     * Data Access Object for this database
+     */
+    @Dao
+    public interface ShareabilityDao {
+        /** Add an AppShareabilityStatus to the database */
+        @Insert(onConflict = OnConflictStrategy.REPLACE)
+        void insertAppStatus(AppShareabilityStatus status);
+
+        /** Add a collection of AppShareabilityStatus objects to the database */
+        @Insert(onConflict = OnConflictStrategy.REPLACE)
+        void insertAppStatuses(AppShareabilityStatus... statuses);
+
+        /**
+         * Update an AppShareabilityStatus in the database
+         * @return The number of entries successfully updated
+         */
+        @Update
+        int updateAppStatus(AppShareabilityStatus status);
+
+        /** Retrieve all entries from the database */
+        @Query("SELECT * FROM AppShareabilityStatus")
+        List<AppShareabilityStatus> getAllEntries();
+    }
+
+    protected abstract ShareabilityDao shareabilityDao();
+}
diff --git a/go/quickstep/src/com/android/launcher3/model/AppShareabilityJobService.java b/go/quickstep/src/com/android/launcher3/model/AppShareabilityJobService.java
new file mode 100644
index 0000000..60bea53
--- /dev/null
+++ b/go/quickstep/src/com/android/launcher3/model/AppShareabilityJobService.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2021 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.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.ComponentName;
+import android.content.Context;
+import android.util.Log;
+
+import com.android.launcher3.R;
+
+/**
+ * A job to request AppShareabilityManager to update its shareability data
+ * The shareability status of an app is not expected to change often, so this job is only
+ * run periodically.
+ */
+public final class AppShareabilityJobService extends JobService {
+
+    private static final String TAG = "AppShareabilityJobService";
+    // Run this job once a week
+    private static final int RECURRENCE_INTERVAL_MILLIS = 604800000;
+
+    @Override
+    public boolean onStartJob(final JobParameters params) {
+        Context context = getApplicationContext();
+        AppShareabilityManager.INSTANCE.get(context).requestFullUpdate();
+        return false; // Job is finished
+    }
+
+    @Override
+    public boolean onStopJob(final JobParameters params) {
+        Log.d(TAG, "App shareability data update job stopped; id=" + params.getJobId()
+                + ", reason="
+                + JobParameters.getInternalReasonCodeDescription(params.getStopReason()));
+        return true; // Reschedule the job
+    }
+
+    /**
+     * Creates and schedules the job.
+     * Does not schedule a duplicate job if one is already pending.
+     * @param context The application context
+     */
+    public static void schedule(Context context) {
+        final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
+
+        final JobInfo pendingJob = jobScheduler.getPendingJob(R.integer.app_shareability_job_id);
+        if (pendingJob != null) {
+            // Don't schedule duplicate jobs
+            return;
+        }
+
+        final JobInfo newJob = new JobInfo.Builder(R.integer.app_shareability_job_id,
+                new ComponentName(context, AppShareabilityJobService.class))
+                .setPeriodic(RECURRENCE_INTERVAL_MILLIS)
+                .setPersisted(true)
+                .setRequiresBatteryNotLow(true)
+                .build();
+        jobScheduler.schedule(newJob);
+    }
+}
diff --git a/go/quickstep/src/com/android/launcher3/model/AppShareabilityManager.java b/go/quickstep/src/com/android/launcher3/model/AppShareabilityManager.java
new file mode 100644
index 0000000..cf80c35
--- /dev/null
+++ b/go/quickstep/src/com/android/launcher3/model/AppShareabilityManager.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2021 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 static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.content.Context;
+import android.content.pm.LauncherActivityInfo;
+import android.content.pm.LauncherApps;
+import android.os.Process;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+import androidx.room.Room;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.launcher3.model.AppShareabilityDatabase.ShareabilityDao;
+import com.android.launcher3.util.MainThreadInitializedObject;
+
+import java.lang.annotation.Retention;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+/**
+ * This class maintains the shareability status of installed apps.
+ * Each app's status is retrieved from the Play Store's API. Statuses are cached in order
+ * to limit extraneous calls to that API (which can be time-consuming).
+ */
+public final class AppShareabilityManager {
+    @Retention(SOURCE)
+    @IntDef({
+        ShareabilityStatus.UNKNOWN,
+        ShareabilityStatus.NOT_SHAREABLE,
+        ShareabilityStatus.SHAREABLE
+    })
+    public @interface ShareabilityStatus {
+        int UNKNOWN = 0;
+        int NOT_SHAREABLE = 1;
+        int SHAREABLE = 2;
+    }
+
+    private static final String TAG = "AppShareabilityManager";
+    private static final String DB_NAME = "shareabilityDatabase";
+    public static MainThreadInitializedObject<AppShareabilityManager> INSTANCE =
+            new MainThreadInitializedObject<>(AppShareabilityManager::new);
+
+    private final Context mContext;
+    // Local map to store the data in memory for quick access
+    private final Map<String, Integer> mDataMap;
+    // Database to persist the data across reboots
+    private AppShareabilityDatabase mDatabase;
+    // Data Access Object for the database
+    private ShareabilityDao mDao;
+    // Class to perform shareability checks
+    private AppShareabilityChecker mShareChecker;
+
+    private AppShareabilityManager(Context context) {
+        mContext = context;
+        mDataMap = new ArrayMap<>();
+        mDatabase = Room.databaseBuilder(mContext, AppShareabilityDatabase.class, DB_NAME).build();
+        mDao = mDatabase.shareabilityDao();
+        MODEL_EXECUTOR.post(this::readFromDB);
+    }
+
+    /**
+     * Set the shareability checker. The checker determines whether given apps are shareable.
+     * This must be set before the manager can update its data.
+     * @param checker Implementation of AppShareabilityChecker to perform the checks
+     */
+    public void setShareabilityChecker(AppShareabilityChecker checker) {
+        mShareChecker = checker;
+    }
+
+    /**
+     * Retrieve the ShareabilityStatus of an app from the local map
+     * This does not interact with the saved database
+     * @param packageName The app's package name
+     * @return The status as a ShareabilityStatus integer
+     */
+    public synchronized @ShareabilityStatus int getStatus(String packageName) {
+        @ShareabilityStatus int status = ShareabilityStatus.UNKNOWN;
+        if (mDataMap.containsKey(packageName)) {
+            status = mDataMap.get(packageName);
+        }
+        return status;
+    }
+
+    /**
+     * Set the status of a given app. This updates the local map as well as the saved database.
+     */
+    public synchronized void setStatus(String packageName, @ShareabilityStatus int status) {
+        mDataMap.put(packageName, status);
+
+        // Write to the database on a separate thread
+        MODEL_EXECUTOR.post(() ->
+                mDao.insertAppStatus(new AppShareabilityStatus(packageName, status)));
+    }
+
+    /**
+     * Set the statuses of given apps. This updates the local map as well as the saved database.
+     */
+    public synchronized void setStatuses(List<AppShareabilityStatus> statuses) {
+        for (int i = 0, size = statuses.size(); i < size; i++) {
+            AppShareabilityStatus entry = statuses.get(i);
+            mDataMap.put(entry.packageName, entry.status);
+        }
+
+        // Write to the database on a separate thread
+        MODEL_EXECUTOR.post(() ->
+                mDao.insertAppStatuses(statuses.toArray(new AppShareabilityStatus[0])));
+    }
+
+    /**
+     * Request a status update for a specific app
+     * @param packageName The app's package name
+     * @param callback Optional callback to be called when the update is complete. The received
+     *                 Boolean denotes whether the update was successful.
+     */
+    public void requestAppStatusUpdate(String packageName, @Nullable Consumer<Boolean> callback) {
+        MODEL_EXECUTOR.post(() -> updateCache(packageName, callback));
+    }
+
+    /**
+     * Request a status update for all apps
+     */
+    public void requestFullUpdate() {
+        MODEL_EXECUTOR.post(this::updateCache);
+    }
+
+    /**
+     * Update the cached shareability data for all installed apps
+     */
+    @WorkerThread
+    private void updateCache() {
+        updateCache(/* packageName */ null, /* callback */ null);
+    }
+
+    /**
+     * Update the cached shareability data
+     * @param packageName A specific package to update. If null, all installed apps will be updated.
+     * @param callback Optional callback to be called when the update is complete. The received
+     *                 Boolean denotes whether the update was successful.
+     */
+    @WorkerThread
+    private void updateCache(@Nullable String packageName, @Nullable Consumer<Boolean> callback) {
+        if (mShareChecker == null) {
+            Log.e(TAG, "AppShareabilityChecker not set");
+            return;
+        }
+
+        List<String> packageNames = new ArrayList<>();
+        if (packageName != null) {
+            packageNames.add(packageName);
+        } else {
+            LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class);
+            List<LauncherActivityInfo> installedApps =
+                    launcherApps.getActivityList(/* packageName */ null, Process.myUserHandle());
+            for (int i = 0, size = installedApps.size(); i < size; i++) {
+                packageNames.add(installedApps.get(i).getApplicationInfo().packageName);
+            }
+        }
+
+        mShareChecker.checkApps(packageNames, this, callback);
+    }
+
+    @WorkerThread
+    private synchronized void readFromDB() {
+        mDataMap.clear();
+        List<AppShareabilityStatus> entries = mDao.getAllEntries();
+        for (int i = 0, size = entries.size(); i < size; i++) {
+            AppShareabilityStatus entry = entries.get(i);
+            mDataMap.put(entry.packageName, entry.status);
+        }
+    }
+
+    /**
+     * Provides a testable instance of this class
+     * This instance allows database queries on the main thread
+     * @hide */
+    @VisibleForTesting
+    public static AppShareabilityManager getTestInstance(Context context) {
+        AppShareabilityManager manager = new AppShareabilityManager(context);
+        manager.mDatabase.close();
+        manager.mDatabase = Room.inMemoryDatabaseBuilder(context, AppShareabilityDatabase.class)
+                .allowMainThreadQueries()
+                .build();
+        manager.mDao = manager.mDatabase.shareabilityDao();
+        return manager;
+    }
+}
diff --git a/go/quickstep/src/com/android/launcher3/model/AppShareabilityStatus.java b/go/quickstep/src/com/android/launcher3/model/AppShareabilityStatus.java
new file mode 100644
index 0000000..61018c6
--- /dev/null
+++ b/go/quickstep/src/com/android/launcher3/model/AppShareabilityStatus.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2021 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 androidx.annotation.NonNull;
+import androidx.room.Entity;
+import androidx.room.PrimaryKey;
+
+import com.android.launcher3.model.AppShareabilityManager.ShareabilityStatus;
+
+/**
+ * Database entry to hold the shareability status of a single app
+ */
+@Entity
+public class AppShareabilityStatus {
+    @PrimaryKey
+    @NonNull
+    public String packageName;
+
+    public @ShareabilityStatus int status;
+
+    public AppShareabilityStatus(@NonNull String packageName, @ShareabilityStatus int status) {
+        this.packageName = packageName;
+        this.status = status;
+    }
+}
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index 454dc6e..20facbe 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -365,6 +365,11 @@
     }
 
     private void initializeSystemShortcut(int resId, ViewGroup container, SystemShortcut info) {
+        if (!info.isEnabled()) {
+            // If the shortcut is disabled, do not display it
+            return;
+        }
+
         View view = inflateAndAdd(
                 resId, container, getInsertIndexForSystemShortcut(container, info));
         if (view instanceof DeepShortcutView) {
@@ -598,6 +603,12 @@
                         NotificationKeyData.extractKeysOnly(dotInfo.getNotificationKeys()));
             }
         }
+
+        @Override
+        public void onSystemShortcutsUpdated() {
+            close(true);
+            PopupContainerWithArrow.showForIcon(mOriginalIcon);
+        }
     }
 
     /**
diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java
index 6f9f0d7..80ffecc 100644
--- a/src/com/android/launcher3/popup/PopupDataProvider.java
+++ b/src/com/android/launcher3/popup/PopupDataProvider.java
@@ -264,6 +264,13 @@
         writer.println(prefix + "\tmPackageUserToDotInfos:" + mPackageUserToDotInfos);
     }
 
+    /**
+     * Tells the listener that the system shortcuts have been updated, causing them to be redrawn.
+     */
+    public void redrawSystemShortcuts() {
+        mChangeListener.onSystemShortcutsUpdated();
+    }
+
     public interface PopupDataChangeListener {
 
         PopupDataChangeListener INSTANCE = new PopupDataChangeListener() { };
@@ -276,5 +283,8 @@
 
         /** A callback to get notified when recommended widgets are bound. */
         default void onRecommendedWidgetsBound() { }
+
+        /** A callback to get notified when system shortcuts have been updated. */
+        default void onSystemShortcutsUpdated() { }
     }
 }