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.quickstep;
18 
19 import static android.view.Surface.ROTATION_0;
20 
21 import static com.android.quickstep.views.OverviewActionsView.DISABLED_NO_THUMBNAIL;
22 import static com.android.quickstep.views.OverviewActionsView.DISABLED_ROTATED;
23 
24 import static java.lang.annotation.RetentionPolicy.SOURCE;
25 
26 import android.annotation.SuppressLint;
27 import android.app.AlertDialog;
28 import android.app.assist.AssistContent;
29 import android.content.ActivityNotFoundException;
30 import android.content.ContentResolver;
31 import android.content.Context;
32 import android.content.Intent;
33 import android.content.SharedPreferences;
34 import android.graphics.Color;
35 import android.graphics.Matrix;
36 import android.graphics.drawable.ColorDrawable;
37 import android.os.Bundle;
38 import android.os.Handler;
39 import android.os.SystemClock;
40 import android.os.UserManager;
41 import android.provider.Settings;
42 import android.text.TextUtils;
43 import android.util.Log;
44 import android.view.LayoutInflater;
45 import android.view.View;
46 import android.widget.Button;
47 import android.widget.TextView;
48 
49 import androidx.annotation.IntDef;
50 import androidx.annotation.VisibleForTesting;
51 
52 import com.android.launcher3.BaseActivity;
53 import com.android.launcher3.LauncherPrefs;
54 import com.android.launcher3.R;
55 import com.android.launcher3.views.ArrowTipView;
56 import com.android.quickstep.util.AssistContentRequester;
57 import com.android.quickstep.util.RecentsOrientedState;
58 import com.android.quickstep.views.GoOverviewActionsView;
59 import com.android.quickstep.views.TaskView.TaskContainer;
60 import com.android.systemui.shared.recents.model.Task;
61 import com.android.systemui.shared.recents.model.ThumbnailData;
62 
63 import java.lang.annotation.Retention;
64 
65 /**
66  * Go-specific extension of the factory class that adds an overlay to TaskView
67  */
68 public final class TaskOverlayFactoryGo extends TaskOverlayFactory {
69     public static final String ACTION_LISTEN = "com.android.quickstep.ACTION_LISTEN";
70     public static final String ACTION_TRANSLATE = "com.android.quickstep.ACTION_TRANSLATE";
71     public static final String ACTION_SEARCH = "com.android.quickstep.ACTION_SEARCH";
72     public static final String ELAPSED_NANOS = "niu_actions_elapsed_realtime_nanos";
73     public static final String ACTIONS_URL = "niu_actions_app_url";
74     public static final String ACTIONS_APP_PACKAGE = "niu_actions_app_package";
75     public static final String ACTIONS_ERROR_CODE = "niu_actions_app_error_code";
76     public static final int ERROR_PERMISSIONS_STRUCTURE = 1;
77     public static final int ERROR_PERMISSIONS_SCREENSHOT = 2;
78     public static final String NIU_ACTIONS_CONFIRMED = "launcher_go.niu_actions_confirmed";
79     private static final String ASSIST_SETTINGS_ARGS_BUNDLE = ":settings:show_fragment_args";
80     private static final String ASSIST_SETTINGS_ARGS_KEY = ":settings:fragment_args_key";
81     private static final String ASSIST_SETTINGS_PREFERENCE_KEY = "default_assist";
82     private static final String TAG = "TaskOverlayFactoryGo";
83 
84     public static final String LISTEN_TOOL_TIP_SEEN = "launcher.go_listen_tip_seen";
85     public static final String TRANSLATE_TOOL_TIP_SEEN = "launcher.go_translate_tip_seen";
86 
87     @Retention(SOURCE)
88     @IntDef({PRIVACY_CONFIRMATION, ASSISTANT_NOT_SELECTED, ASSISTANT_NOT_SUPPORTED})
89     @VisibleForTesting
90     public @interface DialogType{}
91     public static final int PRIVACY_CONFIRMATION = 0;
92     public static final int ASSISTANT_NOT_SELECTED = 1;
93     public static final int ASSISTANT_NOT_SUPPORTED = 2;
94 
95     private AssistContentRequester mContentRequester;
96 
TaskOverlayFactoryGo(Context context)97     public TaskOverlayFactoryGo(Context context) {
98         mContentRequester = new AssistContentRequester(context);
99     }
100 
101     /**
102      * Create a new overlay instance for the given View
103      */
createOverlay(TaskContainer taskContainer)104     public TaskOverlayGo createOverlay(TaskContainer taskContainer) {
105         return new TaskOverlayGo(taskContainer, mContentRequester);
106     }
107 
108     /**
109      * Overlay on each task handling Overview Action Buttons.
110      * @param <T> The type of View in which the overlay will be placed
111      */
112     public static final class TaskOverlayGo<T extends GoOverviewActionsView> extends TaskOverlay {
113         private String mNIUPackageName;
114         private String mTaskPackageName;
115         private String mWebUrl;
116         private boolean mAssistStructurePermitted;
117         private boolean mAssistScreenshotPermitted;
118         private AssistContentRequester mFactoryContentRequester;
119         private SharedPreferences mSharedPreferences;
120         private OverlayDialogGo mDialog;
121         private ArrowTipView mArrowTipView;
122 
TaskOverlayGo(TaskContainer taskContainer, AssistContentRequester assistContentRequester)123         private TaskOverlayGo(TaskContainer taskContainer,
124                 AssistContentRequester assistContentRequester) {
125             super(taskContainer);
126             mFactoryContentRequester = assistContentRequester;
127             mSharedPreferences = LauncherPrefs.getPrefs(mApplicationContext);
128         }
129 
130         /**
131          * Called when the current task is interactive for the user
132          */
133         @Override
initOverlay(Task task, ThumbnailData thumbnail, Matrix matrix, boolean rotated)134         public void initOverlay(Task task, ThumbnailData thumbnail, Matrix matrix,
135                 boolean rotated) {
136             if (mDialog != null && mDialog.isShowing()) {
137                 // Redraw the dialog in case the layout changed
138                 mDialog.dismiss();
139                 showDialog(mDialog.getAction(), mDialog.getType());
140             }
141 
142             getActionsView().updateDisabledFlags(DISABLED_NO_THUMBNAIL, thumbnail == null);
143             if (thumbnail == null) {
144                 return;
145             }
146 
147             getActionsView().updateDisabledFlags(DISABLED_ROTATED, rotated);
148             // Disable Overview Actions for Work Profile apps
149             boolean isManagedProfileTask =
150                     UserManager.get(mApplicationContext).isManagedProfile(task.key.userId);
151             boolean isAllowedByPolicy = mTaskContainer.getThumbnailViewDeprecated().isRealSnapshot()
152                     && !isManagedProfileTask;
153             getActionsView().setCallbacks(new OverlayUICallbacksGoImpl(isAllowedByPolicy, task));
154             mTaskPackageName = task.key.getPackageName();
155             mSharedPreferences = LauncherPrefs.getPrefs(mApplicationContext);
156             checkSettings();
157 
158             if (!mAssistStructurePermitted || !mAssistScreenshotPermitted
159                     || TextUtils.isEmpty(mNIUPackageName)) {
160                 return;
161             }
162 
163             int taskId = task.key.id;
164             mFactoryContentRequester.requestAssistContent(taskId, this::onAssistContentReceived);
165 
166             RecentsOrientedState orientedState = mTaskContainer.getTaskView().getOrientedState();
167             boolean isInLandscape = orientedState.getDisplayRotation() != ROTATION_0;
168 
169             // show tooltips in portrait mode only
170             // TODO: remove If check once b/183714277 is fixed
171             if (!isInLandscape) {
172                 new Handler().post(() -> {
173                     showTooltipsIfUnseen();
174                 });
175             }
176         }
177 
178         /** Provide Assist Content to the overlay. */
179         @VisibleForTesting
onAssistContentReceived(AssistContent assistContent)180         public void onAssistContentReceived(AssistContent assistContent) {
181             mWebUrl = assistContent.getWebUri() != null
182                     ? assistContent.getWebUri().toString() : null;
183         }
184 
185         @Override
reset()186         public void reset() {
187             super.reset();
188             mWebUrl = null;
189             if (mDialog != null && mDialog.isShowing()) {
190                 mDialog.dismiss();
191             }
192         }
193 
194         @Override
updateOrientationState(RecentsOrientedState state)195         public void updateOrientationState(RecentsOrientedState state) {
196             super.updateOrientationState(state);
197             ((GoOverviewActionsView) getActionsView()).updateOrientationState(state);
198         }
199 
200         /**
201          * Creates and sends an Intent corresponding to the button that was clicked
202          */
sendNIUIntent(String actionType)203         private void sendNIUIntent(String actionType) {
204             if (TextUtils.isEmpty(mNIUPackageName)) {
205                 showDialog(actionType, ASSISTANT_NOT_SELECTED);
206                 return;
207             }
208 
209             if (!mSharedPreferences.getBoolean(NIU_ACTIONS_CONFIRMED, false)) {
210                 showDialog(actionType, PRIVACY_CONFIRMATION);
211                 return;
212             }
213 
214             Intent intent = createNIUIntent(actionType);
215             // Only add and send the image if the appropriate permissions are held
216             if (mAssistStructurePermitted && mAssistScreenshotPermitted) {
217                 mImageApi.shareAsDataWithExplicitIntent(/* crop */ null, intent,
218                         () -> showDialog(actionType, ASSISTANT_NOT_SUPPORTED));
219             } else {
220                 // If both permissions are disabled, the structure error code takes priority
221                 // The user must enable that one before they can enable screenshots
222                 int code = mAssistStructurePermitted ? ERROR_PERMISSIONS_SCREENSHOT
223                         : ERROR_PERMISSIONS_STRUCTURE;
224                 intent.putExtra(ACTIONS_ERROR_CODE, code);
225                 try {
226                     mApplicationContext.startActivity(intent);
227                 } catch (ActivityNotFoundException e) {
228                     Log.e(TAG, "No activity found to receive permission error intent");
229                     showDialog(actionType, ASSISTANT_NOT_SUPPORTED);
230                 }
231             }
232         }
233 
createNIUIntent(String actionType)234         private Intent createNIUIntent(String actionType) {
235             Intent intent = new Intent(actionType)
236                     .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK)
237                     .addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
238                     .setPackage(mNIUPackageName)
239                     .putExtra(ACTIONS_APP_PACKAGE, mTaskPackageName)
240                     .putExtra(ELAPSED_NANOS, SystemClock.elapsedRealtimeNanos());
241 
242             if (mWebUrl != null) {
243                 intent.putExtra(ACTIONS_URL, mWebUrl);
244             }
245 
246             return intent;
247         }
248 
249         /**
250          * Checks whether the Assistant has screen context permissions
251          */
252         @VisibleForTesting
checkSettings()253         public void checkSettings() {
254             ContentResolver contentResolver = mApplicationContext.getContentResolver();
255             mAssistStructurePermitted = Settings.Secure.getInt(contentResolver,
256                     Settings.Secure.ASSIST_STRUCTURE_ENABLED, 1) != 0;
257             mAssistScreenshotPermitted = Settings.Secure.getInt(contentResolver,
258                     Settings.Secure.ASSIST_SCREENSHOT_ENABLED, 1) != 0;
259 
260             String assistantPackage =
261                     Settings.Secure.getString(contentResolver, Settings.Secure.ASSISTANT);
262             if (!TextUtils.isEmpty(assistantPackage)) {
263                 mNIUPackageName = assistantPackage.split("/", 2)[0];
264             } else {
265                 mNIUPackageName = "";
266             }
267         }
268 
269         protected class OverlayUICallbacksGoImpl extends OverlayUICallbacksImpl
270                 implements OverlayUICallbacksGo {
OverlayUICallbacksGoImpl(boolean isAllowedByPolicy, Task task)271             public OverlayUICallbacksGoImpl(boolean isAllowedByPolicy, Task task) {
272                 super(isAllowedByPolicy, task);
273             }
274 
275             @SuppressLint("NewApi")
onListen()276             public void onListen() {
277                 if (mIsAllowedByPolicy) {
278                     endLiveTileMode(() -> sendNIUIntent(ACTION_LISTEN));
279                 } else {
280                     showBlockedByPolicyMessage();
281                 }
282             }
283 
284             @SuppressLint("NewApi")
onTranslate()285             public void onTranslate() {
286                 if (mIsAllowedByPolicy) {
287                     endLiveTileMode(() -> sendNIUIntent(ACTION_TRANSLATE));
288                 } else {
289                     showBlockedByPolicyMessage();
290                 }
291             }
292 
293             @SuppressLint("NewApi")
onSearch()294             public void onSearch() {
295                 if (mIsAllowedByPolicy) {
296                     endLiveTileMode(() -> sendNIUIntent(ACTION_SEARCH));
297                 } else {
298                     showBlockedByPolicyMessage();
299                 }
300             }
301         }
302 
303         @VisibleForTesting
setImageActionsAPI(ImageActionsApi imageActionsApi)304         public void setImageActionsAPI(ImageActionsApi imageActionsApi) {
305             mImageApi = imageActionsApi;
306         }
307 
showDialog(String action, @DialogType int type)308         private void showDialog(String action, @DialogType int type) {
309             switch (type) {
310                 case PRIVACY_CONFIRMATION:
311                     showDialog(action, PRIVACY_CONFIRMATION,
312                             R.string.niu_actions_confirmation_title,
313                             R.string.niu_actions_confirmation_text, R.string.dialog_cancel,
314                             this::onDialogClickCancel, R.string.dialog_acknowledge,
315                             this::onNiuActionsConfirmationAccept);
316                     break;
317                 case ASSISTANT_NOT_SELECTED:
318                     showDialog(action, ASSISTANT_NOT_SELECTED,
319                             R.string.assistant_not_selected_title,
320                             R.string.assistant_not_selected_text, R.string.dialog_cancel,
321                             this::onDialogClickCancel, R.string.dialog_settings,
322                             this::onDialogClickSettings);
323                     break;
324                 case ASSISTANT_NOT_SUPPORTED:
325                     showDialog(action, ASSISTANT_NOT_SUPPORTED,
326                             R.string.assistant_not_supported_title,
327                             R.string.assistant_not_supported_text, R.string.dialog_cancel,
328                             this::onDialogClickCancel, R.string.dialog_settings,
329                             this::onDialogClickSettings);
330                     break;
331                 default:
332                     Log.e(TAG, "Unexpected dialog type");
333             }
334         }
335 
showDialog(String action, @DialogType int type, int titleTextID, int bodyTextID, int button1TextID, View.OnClickListener button1Callback, int button2TextID, View.OnClickListener button2Callback)336         private void showDialog(String action, @DialogType int type, int titleTextID,
337                                 int bodyTextID, int button1TextID,
338                                 View.OnClickListener button1Callback, int button2TextID,
339                                 View.OnClickListener button2Callback) {
340             BaseActivity activity = BaseActivity.fromContext(getActionsView().getContext());
341             LayoutInflater inflater = LayoutInflater.from(activity);
342             View view = inflater.inflate(R.layout.niu_actions_dialog, /* root */ null);
343 
344             TextView dialogTitle = view.findViewById(R.id.niu_actions_dialog_header);
345             dialogTitle.setText(titleTextID);
346 
347             TextView dialogBody = view.findViewById(R.id.niu_actions_dialog_description);
348             dialogBody.setText(bodyTextID);
349 
350             Button button1 = view.findViewById(R.id.niu_actions_dialog_button_1);
351             button1.setText(button1TextID);
352             button1.setOnClickListener(button1Callback);
353 
354             Button button2 = view.findViewById(R.id.niu_actions_dialog_button_2);
355             button2.setText(button2TextID);
356             button2.setOnClickListener(button2Callback);
357 
358             mDialog = new OverlayDialogGo(activity, type, action);
359             mDialog.setView(view);
360             mDialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
361             mDialog.show();
362         }
363 
onNiuActionsConfirmationAccept(View v)364         private void onNiuActionsConfirmationAccept(View v) {
365             mDialog.dismiss();
366             mSharedPreferences.edit().putBoolean(NIU_ACTIONS_CONFIRMED, true).apply();
367             sendNIUIntent(mDialog.getAction());
368         }
369 
onDialogClickCancel(View v)370         private void onDialogClickCancel(View v) {
371             mDialog.cancel();
372         }
373 
374         @VisibleForTesting
getDialog()375         public OverlayDialogGo getDialog() {
376             return mDialog;
377         }
378 
onDialogClickSettings(View v)379         private void onDialogClickSettings(View v) {
380             mDialog.dismiss();
381 
382             Bundle fragmentArgs = new Bundle();
383             fragmentArgs.putString(ASSIST_SETTINGS_ARGS_KEY, ASSIST_SETTINGS_PREFERENCE_KEY);
384             Intent intent = new Intent(Settings.ACTION_VOICE_INPUT_SETTINGS)
385                     .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK)
386                     .putExtra(ASSIST_SETTINGS_ARGS_BUNDLE, fragmentArgs);
387             try {
388                 mApplicationContext.startActivity(intent);
389             } catch (ActivityNotFoundException e) {
390                 Log.e(TAG, "No activity found to receive assistant settings intent");
391             }
392         }
393 
394         /**
395          * Checks and Shows the tooltip if they are not seen by user
396          * Order of tooltips are translate and then listen
397          */
showTooltipsIfUnseen()398         private void showTooltipsIfUnseen() {
399             if (mArrowTipView != null && mArrowTipView.isOpen()) {
400                 return;
401             }
402             if (!mSharedPreferences.getBoolean(TRANSLATE_TOOL_TIP_SEEN, false)) {
403                 mArrowTipView = ((GoOverviewActionsView) getActionsView()).showTranslateToolTip();
404                 mSharedPreferences.edit().putBoolean(TRANSLATE_TOOL_TIP_SEEN, true).apply();
405             } else if (!mSharedPreferences.getBoolean(LISTEN_TOOL_TIP_SEEN, false)) {
406                 mArrowTipView = ((GoOverviewActionsView) getActionsView()).showListenToolTip();
407                 mSharedPreferences.edit().putBoolean(LISTEN_TOOL_TIP_SEEN, true).apply();
408             }
409         }
410     }
411 
412     /**
413      * Basic modal dialog for various user prompts
414      */
415     @VisibleForTesting
416     public static final class OverlayDialogGo extends AlertDialog {
417         private final String mAction;
418         private final @DialogType int mType;
419 
OverlayDialogGo(Context context, @DialogType int type, String action)420         OverlayDialogGo(Context context, @DialogType int type, String action) {
421             super(context);
422             mType = type;
423             mAction = action;
424         }
425 
getAction()426         public String getAction() {
427             return mAction;
428         }
getType()429         public @DialogType int getType() {
430             return mType;
431         }
432     }
433 
434     /**
435      * Callbacks the Ui can generate. This is the only way for a Ui to call methods on the
436      * controller.
437      */
438     public interface OverlayUICallbacksGo extends OverlayUICallbacks {
439         /** User has requested to listen to the current content read aloud */
onListen()440         void onListen();
441 
442         /** User has requested a translation of the current content */
onTranslate()443         void onTranslate();
444 
445         /** User has requested a visual search of the current content */
onSearch()446         void onSearch();
447     }
448 }
449