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