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