• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.clipboardoverlay;
18 
19 import static android.content.ClipDescription.CLASSIFICATION_COMPLETE;
20 
21 import static com.android.systemui.Flags.clipboardNoninteractiveOnLockscreen;
22 import static com.android.systemui.Flags.clipboardOverlayMultiuser;
23 import static com.android.systemui.Flags.overrideSuppressOverlayCondition;
24 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED;
25 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED;
26 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_TOAST_SHOWN;
27 
28 import static com.google.android.setupcompat.util.WizardManagerHelper.SETTINGS_SECURE_USER_SETUP_COMPLETE;
29 
30 import android.app.KeyguardManager;
31 import android.content.ClipData;
32 import android.content.ClipboardManager;
33 import android.content.Context;
34 import android.os.Build;
35 import android.os.UserHandle;
36 import android.provider.Settings;
37 import android.util.Log;
38 
39 import androidx.annotation.NonNull;
40 
41 import com.android.internal.annotations.VisibleForTesting;
42 import com.android.internal.logging.UiEventLogger;
43 import com.android.systemui.CoreStartable;
44 import com.android.systemui.dagger.SysUISingleton;
45 import com.android.systemui.dagger.qualifiers.Main;
46 import com.android.systemui.settings.UserTracker;
47 import com.android.systemui.user.utils.UserScopedService;
48 
49 import java.util.concurrent.Executor;
50 
51 import javax.inject.Inject;
52 import javax.inject.Provider;
53 
54 /**
55  * ClipboardListener brings up a clipboard overlay when something is copied to the clipboard.
56  */
57 @SysUISingleton
58 public class ClipboardListener implements
59         CoreStartable, ClipboardManager.OnPrimaryClipChangedListener {
60     private static final String TAG = "ClipboardListener";
61 
62     @VisibleForTesting
63     static final String SHELL_PACKAGE = "com.android.shell";
64     @VisibleForTesting
65     static final String EXTRA_SUPPRESS_OVERLAY =
66             "com.android.systemui.SUPPRESS_CLIPBOARD_OVERLAY";
67 
68     private final Context mContext;
69     private final Provider<ClipboardOverlayController> mOverlayProvider;
70     private final ClipboardToast mClipboardToast;
71     private final UserScopedService<ClipboardManager> mClipboardManagerProvider;
72     private final UserScopedService<KeyguardManager> mKeyguardManagerProvider;
73     private final UiEventLogger mUiEventLogger;
74     private final ClipboardOverlaySuppressionController mClipboardOverlaySuppressionController;
75     private ClipboardOverlay mClipboardOverlay;
76     private ClipboardManager mClipboardManagerForUser;
77     private KeyguardManager mKeyguardManagerForUser;
78 
79     private final UserTracker mUserTracker;
80     private final Executor mMainExecutor;
81 
82     private final UserTracker.Callback mCallback = new UserTracker.Callback() {
83         @Override
84         public void onUserChanged(int newUser, @NonNull Context userContext) {
85             UserTracker.Callback.super.onUserChanged(newUser, userContext);
86             mClipboardManagerForUser.removePrimaryClipChangedListener(ClipboardListener.this);
87             setUser(mUserTracker.getUserHandle());
88             mClipboardManagerForUser.addPrimaryClipChangedListener(ClipboardListener.this);
89         }
90     };
91 
92     @Inject
ClipboardListener(Context context, Provider<ClipboardOverlayController> clipboardOverlayControllerProvider, ClipboardToast clipboardToast, UserTracker userTracker, UserScopedService<ClipboardManager> clipboardManager, UserScopedService<KeyguardManager> keyguardManager, UiEventLogger uiEventLogger, @Main Executor mainExecutor, ClipboardOverlaySuppressionController clipboardOverlaySuppressionController)93     public ClipboardListener(Context context,
94             Provider<ClipboardOverlayController> clipboardOverlayControllerProvider,
95             ClipboardToast clipboardToast,
96             UserTracker userTracker,
97             UserScopedService<ClipboardManager> clipboardManager,
98             UserScopedService<KeyguardManager> keyguardManager,
99             UiEventLogger uiEventLogger,
100             @Main Executor mainExecutor,
101             ClipboardOverlaySuppressionController clipboardOverlaySuppressionController) {
102         mContext = context;
103         mOverlayProvider = clipboardOverlayControllerProvider;
104         mClipboardToast = clipboardToast;
105         mClipboardManagerProvider = clipboardManager;
106         mKeyguardManagerProvider = keyguardManager;
107         mUiEventLogger = uiEventLogger;
108         mClipboardOverlaySuppressionController = clipboardOverlaySuppressionController;
109 
110         mMainExecutor = mainExecutor;
111         mUserTracker = userTracker;
112         setUser(mUserTracker.getUserHandle());
113     }
114 
setUser(UserHandle user)115     private void setUser(UserHandle user) {
116         mClipboardManagerForUser = mClipboardManagerProvider.forUser(user);
117         mKeyguardManagerForUser = mKeyguardManagerProvider.forUser(user);
118     }
119 
120     @Override
start()121     public void start() {
122         if (clipboardOverlayMultiuser()) {
123             mUserTracker.addCallback(mCallback, mMainExecutor);
124         }
125         mClipboardManagerForUser.addPrimaryClipChangedListener(this);
126     }
127 
128     @Override
onPrimaryClipChanged()129     public void onPrimaryClipChanged() {
130         if (!mClipboardManagerForUser.hasPrimaryClip()) {
131             return;
132         }
133 
134         String clipSource = mClipboardManagerForUser.getPrimaryClipSource();
135         ClipData clipData = mClipboardManagerForUser.getPrimaryClip();
136 
137         if (overrideSuppressOverlayCondition()) {
138             if (mClipboardOverlaySuppressionController.shouldSuppressOverlay(clipData, clipSource,
139                     Build.IS_EMULATOR)) {
140                 Log.i(TAG, "Clipboard overlay suppressed.");
141                 return;
142             }
143         } else {
144             if (shouldSuppressOverlay(clipData, clipSource, Build.IS_EMULATOR)) {
145                 Log.i(TAG, "Clipboard overlay suppressed.");
146                 return;
147             }
148         }
149 
150         // user should not access intents before setup or while device is locked
151         if ((clipboardNoninteractiveOnLockscreen() && mKeyguardManagerForUser.isDeviceLocked())
152                 || !isUserSetupComplete()
153                 || clipData == null // shouldn't happen, but just in case
154                 || clipData.getItemCount() == 0) {
155             if (shouldShowToast(clipData)) {
156                 mUiEventLogger.log(CLIPBOARD_TOAST_SHOWN, 0, clipSource);
157                 mClipboardToast.showCopiedToast();
158             }
159             return;
160         }
161 
162         if (mClipboardOverlay == null) {
163             mClipboardOverlay = mOverlayProvider.get();
164             mUiEventLogger.log(CLIPBOARD_OVERLAY_ENTERED, 0, clipSource);
165         } else {
166             mUiEventLogger.log(CLIPBOARD_OVERLAY_UPDATED, 0, clipSource);
167         }
168         mClipboardOverlay.setClipData(clipData, clipSource);
169         mClipboardOverlay.setOnSessionCompleteListener(() -> {
170             // Session is complete, free memory until it's needed again.
171             mClipboardOverlay = null;
172         });
173     }
174 
175     // The overlay is suppressed if EXTRA_SUPPRESS_OVERLAY is true and the device is an emulator or
176     // the source package is SHELL_PACKAGE. This is meant to suppress the overlay when the emulator
177     // or a mirrored device is syncing the clipboard.
178     @VisibleForTesting
shouldSuppressOverlay(ClipData clipData, String clipSource, boolean isEmulator)179     static boolean shouldSuppressOverlay(ClipData clipData, String clipSource,
180             boolean isEmulator) {
181         if (!(isEmulator || SHELL_PACKAGE.equals(clipSource))) {
182             return false;
183         }
184         if (clipData == null || clipData.getDescription().getExtras() == null) {
185             return false;
186         }
187         return clipData.getDescription().getExtras().getBoolean(EXTRA_SUPPRESS_OVERLAY, false);
188     }
189 
shouldShowToast(ClipData clipData)190     boolean shouldShowToast(ClipData clipData) {
191         if (clipData == null) {
192             return false;
193         } else if (clipData.getDescription().getClassificationStatus() == CLASSIFICATION_COMPLETE) {
194             // only show for classification complete if we aren't already showing a toast, to ignore
195             // the duplicate ClipData with classification
196             return !mClipboardToast.isShowing();
197         }
198         return true;
199     }
200 
isUserSetupComplete()201     private boolean isUserSetupComplete() {
202         return Settings.Secure.getInt(mContext.getContentResolver(),
203                 SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
204     }
205 
206     interface ClipboardOverlay {
setClipData(ClipData clipData, String clipSource)207         void setClipData(ClipData clipData, String clipSource);
208 
setOnSessionCompleteListener(Runnable runnable)209         void setOnSessionCompleteListener(Runnable runnable);
210     }
211 }
212