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