1 /* 2 * Copyright (C) 2020 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.car.ui.imewidescreen; 18 19 import static com.android.car.ui.core.CarUi.TARGET_API_R; 20 21 import android.annotation.SuppressLint; 22 import android.content.ContentResolver; 23 import android.content.Context; 24 import android.content.pm.ApplicationInfo; 25 import android.content.pm.PackageInfo; 26 import android.content.pm.PackageManager; 27 import android.content.res.Resources; 28 import android.database.Cursor; 29 import android.graphics.Bitmap; 30 import android.graphics.Rect; 31 import android.graphics.drawable.BitmapDrawable; 32 import android.inputmethodservice.ExtractEditText; 33 import android.inputmethodservice.InputMethodService; 34 import android.net.Uri; 35 import android.os.Build; 36 import android.os.Build.VERSION_CODES; 37 import android.os.Bundle; 38 import android.os.IBinder; 39 import android.os.Parcel; 40 import android.text.InputType; 41 import android.text.TextUtils; 42 import android.util.Log; 43 import android.view.Gravity; 44 import android.view.LayoutInflater; 45 import android.view.SurfaceControlViewHost.SurfacePackage; 46 import android.view.SurfaceHolder; 47 import android.view.SurfaceView; 48 import android.view.View; 49 import android.view.ViewGroup; 50 import android.view.inputmethod.EditorInfo; 51 import android.view.inputmethod.InputConnection; 52 import android.widget.FrameLayout; 53 import android.widget.ImageView; 54 import android.widget.TextView; 55 56 import androidx.annotation.DrawableRes; 57 import androidx.annotation.NonNull; 58 import androidx.annotation.Nullable; 59 import androidx.annotation.RequiresApi; 60 import androidx.annotation.VisibleForTesting; 61 import androidx.recyclerview.widget.LinearLayoutManager; 62 63 import com.android.car.ui.CarUiLayoutInflaterFactory; 64 import com.android.car.ui.R; 65 import com.android.car.ui.core.SearchResultsProvider; 66 import com.android.car.ui.recyclerview.CarUiContentListItem; 67 import com.android.car.ui.recyclerview.CarUiListItemAdapter; 68 import com.android.car.ui.recyclerview.CarUiRecyclerView; 69 import com.android.car.ui.utils.CarUiUtils; 70 71 import java.util.ArrayList; 72 import java.util.regex.Matcher; 73 import java.util.regex.Pattern; 74 75 /** 76 * Helper class to build an IME that support widescreen mode. 77 * 78 * <p> This class provides helper methods that should be invoked during the lifecycle of an IME. 79 * Usage of these methods are listed below. 80 * <ul> 81 * <li>create an instance of {@link CarUiImeWideScreenController} in 82 * {@link InputMethodService#onCreate()}</li> 83 * <li>return {@link #onEvaluateFullscreenMode(boolean)} from 84 * {@link InputMethodService#onEvaluateFullscreenMode()}</li> 85 * <li>return the view created by 86 * {@link #createWideScreenImeView(View)} 87 * from {@link InputMethodService#onCreateInputView()}</li> 88 * <li>{@link #onComputeInsets(InputMethodService.Insets) should be called from 89 * {@link InputMethodService#onComputeInsets(InputMethodService.Insets)}</li> 90 * <li>{@link #onAppPrivateCommand(String, Bundle) should be called from 91 * {@link InputMethodService#onAppPrivateCommand(String, Bundle)}}</li> 92 * <li>{@link #setExtractViewShown(boolean)} should be called from 93 * {@link InputMethodService#setExtractViewShown(boolean)}</li> 94 * <li>{@link #onStartInputView(EditorInfo, InputConnection, CharSequence)} should be called 95 * from {@link InputMethodService#onStartInputView(EditorInfo, boolean)}</li> 96 * <li>{@link #onFinishInputView()} should be called from 97 * {@link InputMethodService#onFinishInputView(boolean)}</li> 98 * </ul> 99 * 100 * <p> All the methods in this class are guarded with a check {@link #isWideScreenMode()}. If 101 * wide screen mode is disabled all the method would return without doing anything. Also, IME 102 * should check for {@link #isWideScreenMode()} in 103 * {@link InputMethodService#setExtractViewShown(boolean)} and return the original value instead 104 * of false. for more info see {@link #setExtractViewShown(boolean)} 105 */ 106 @RequiresApi(TARGET_API_R) 107 public class CarUiImeWideScreenController { 108 109 private static final String TAG = "ImeWideScreenController"; 110 private static final String NOT_ASTERISK_OR_CAPTURED_ASTERISK = "[^*]+|(\\*)"; 111 112 // Automotive wide screen mode bundle keys. 113 114 // Action name of the action to support wide screen mode templates data. 115 public static final String WIDE_SCREEN_ACTION = "automotive_wide_screen"; 116 // Action name of action that will be used by IMS to notify the application to clear the data 117 // in the EditText. 118 public static final String WIDE_SCREEN_CLEAR_DATA_ACTION = "automotive_wide_screen_clear_data"; 119 // Action name when user clicks on the back button to close the IME. 120 public static final String WIDE_SCREEN_ON_BACK_CLICKED_ACTION = 121 "automotive_wide_screen_back_clicked"; 122 public static final String WIDE_SCREEN_POST_LOAD_SEARCH_RESULTS_ACTION = 123 "automotive_wide_screen_post_load_search_results"; 124 // Action name used by applications to notify that new search results are available. 125 public static final String WIDE_SCREEN_SEARCH_RESULTS = "wide_screen_search_results"; 126 // Key to provide the resource id for the icon that will be displayed in the input area. If 127 // this is not provided applications icon will be used. Value format is int. 128 public static final String WIDE_SCREEN_EXTRACTED_TEXT_ICON_RES_ID = 129 "extracted_text_icon_res_id"; 130 // key to provide the drawable resource for the icon that will be displayed in the input area. 131 // If this is not provided, applications icon will be used. Value format is byteArray. 132 public static final String WIDE_SCREEN_EXTRACTED_TEXT_ICON = "extracted_text_icon"; 133 // Key to determine if IME should display the content area or not. Content area is referred to 134 // the area used by IME to display search results, description title and description 135 // provided by the application. By default it will be shown but this value could be ignored 136 // if bool/car_ui_ime_wide_screen_allow_app_hide_content_area is set to false. Value format 137 // is boolean. 138 public static final String REQUEST_RENDER_CONTENT_AREA = "request_render_content_area"; 139 // Key used to provide the description title to be rendered in the content area. Value format 140 // is String. 141 public static final String ADD_DESC_TITLE_TO_CONTENT_AREA = "add_desc_title_to_content_area"; 142 // Key used to provide the description to be rendered in the content area. Value format is 143 // String. 144 public static final String ADD_DESC_TO_CONTENT_AREA = "add_desc_to_content_area"; 145 // Key used to provide the error description to be rendered in the input area. Value format 146 // is String. 147 public static final String ADD_ERROR_DESC_TO_INPUT_AREA = "add_error_desc_to_input_area"; 148 149 // wide screen search item keys. Each search item contains a title, sub-title, primary image 150 // and an secondary image. Click actions can be performed on item and secondary image. 151 // Application will be notified with the Ids of item clicked. 152 153 // Each key below represents a list. Search results will be displayed in the same order as 154 // the list provided by the application. For example, to create the search item at index 0 155 // controller will get the information from each lists index 0. 156 157 // Key used to provide list of unique id for each item. This same id will be sent back to 158 // the application when the item is clicked. Value format is ArrayList<String> 159 public static final String SEARCH_RESULT_ITEM_ID_LIST = "search_result_item_id_list"; 160 161 public static final String SEARCH_RESULT_SUPPLEMENTAL_ICON_ID_LIST = 162 "search_result_supplemental_icon_id_list"; 163 // key used to provide the surface package information by the application to the IME. IME 164 // will send the surface info each time its being displayed. 165 public static final String CONTENT_AREA_SURFACE_PACKAGE = "content_area_surface_package"; 166 // key to provide the host token of surface view by IME to the application. 167 public static final String CONTENT_AREA_SURFACE_HOST_TOKEN = "content_area_surface_host_token"; 168 // key to provide the display id of surface view by IME to the application. 169 public static final String CONTENT_AREA_SURFACE_DISPLAY_ID = "content_area_surface_display_id"; 170 // key to provide the height of surface view by IME to the application. 171 public static final String CONTENT_AREA_SURFACE_HEIGHT = "content_area_surface_height"; 172 // key to provide the width of surface view by IME to the application. 173 public static final String CONTENT_AREA_SURFACE_WIDTH = "content_area_surface_width"; 174 175 private View mRootView; 176 private final Context mContext; 177 @Nullable 178 private View mExtractActionAutomotive; 179 @NonNull 180 private View mContentAreaAutomotive; 181 // whether to render the content area for automotive when in wide screen mode. 182 private boolean mImeRendersAllContent = true; 183 private boolean mAllowAppToHideContentArea; 184 @Nullable 185 private ArrayList<CarUiContentListItem> mAutomotiveSearchItems; 186 @NonNull 187 private TextView mWideScreenDescriptionTitle; 188 @NonNull 189 private TextView mWideScreenDescription; 190 @NonNull 191 private TextView mWideScreenErrorMessage; 192 @NonNull 193 private ImageView mWideScreenErrorImage; 194 @NonNull 195 private ImageView mWideScreenClearData; 196 @NonNull 197 private CarUiRecyclerView mRecyclerView; 198 @Nullable 199 private ImageView mWideScreenExtractedTextIcon; 200 private boolean mIsExtractIconProvidedByApp; 201 @NonNull 202 private FrameLayout mInputFrame; 203 @NonNull 204 private ExtractEditText mExtractEditText; 205 @NonNull 206 private EditorInfo mInputEditorInfo; 207 private InputConnection mInputConnection; 208 private boolean mExtractViewHidden; 209 @NonNull 210 private View mFullscreenArea; 211 @NonNull 212 private SurfaceView mContentAreaSurfaceView; 213 @NonNull 214 private FrameLayout mInputExtractEditTextContainer; 215 private final InputMethodService mInputMethodService; 216 CarUiImeWideScreenController(@onNull Context context, @NonNull InputMethodService ims)217 public CarUiImeWideScreenController(@NonNull Context context, @NonNull InputMethodService ims) { 218 mContext = context; 219 mInputMethodService = ims; 220 } 221 222 /** 223 * Create and return the view hierarchy used for the input area in wide screen mode. This method 224 * will inflate the templates with the inputView provided. 225 * 226 * @param inputView view of the keyboard created by application. 227 * @return view to be used by {@link InputMethodService}. 228 */ createWideScreenImeView(@onNull View inputView)229 public View createWideScreenImeView(@NonNull View inputView) { 230 if (!isWideScreenMode()) { 231 return inputView; 232 } 233 234 LayoutInflater inflater = LayoutInflater.from(mContext); 235 if (inflater.getFactory2() == null) { 236 inflater.setFactory2(new CarUiLayoutInflaterFactory()); 237 } 238 239 mRootView = inflater.inflate(R.layout.car_ui_ims_wide_screen_input_view, null); 240 241 mInputFrame = mRootView.requireViewById(R.id.car_ui_wideScreenInputArea); 242 mInputFrame.addView(inputView, new FrameLayout.LayoutParams( 243 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); 244 245 mAllowAppToHideContentArea = 246 mContext.getResources().getBoolean( 247 R.bool.car_ui_ime_wide_screen_allow_app_hide_content_area); 248 249 mContentAreaSurfaceView = mRootView.requireViewById(R.id.car_ui_ime_surface); 250 mContentAreaSurfaceView.getHolder().addCallback(new SurfaceHolder.Callback() { 251 @Override 252 public void surfaceCreated(SurfaceHolder holder) { 253 } 254 255 @Override 256 public void surfaceChanged(SurfaceHolder holder, int format, 257 int width, int height) { 258 Bundle bundle = new Bundle(); 259 bundle.putInt(CONTENT_AREA_SURFACE_HEIGHT, 260 mContentAreaSurfaceView.getHeight()); 261 bundle.putInt(CONTENT_AREA_SURFACE_WIDTH, mContentAreaSurfaceView.getWidth()); 262 mInputConnection.performPrivateCommand(WIDE_SCREEN_ACTION, bundle); 263 } 264 265 @Override 266 public void surfaceDestroyed(SurfaceHolder holder) { 267 } 268 }); 269 mContentAreaSurfaceView.setZOrderOnTop(true); 270 mWideScreenDescriptionTitle = 271 mRootView.requireViewById(R.id.car_ui_wideScreenDescriptionTitle); 272 mWideScreenDescription = mRootView.requireViewById(R.id.car_ui_wideScreenDescription); 273 mExtractActionAutomotive = 274 mRootView.findViewById(R.id.car_ui_inputExtractActionAutomotive); 275 mContentAreaAutomotive = mRootView.requireViewById(R.id.car_ui_contentAreaAutomotive); 276 mRecyclerView = mRootView.requireViewById(R.id.car_ui_wideScreenSearchResultList); 277 mWideScreenErrorMessage = mRootView.requireViewById(R.id.car_ui_wideScreenErrorMessage); 278 mWideScreenExtractedTextIcon = 279 mRootView.findViewById(R.id.car_ui_wideScreenExtractedTextIcon); 280 mWideScreenErrorImage = mRootView.requireViewById(R.id.car_ui_wideScreenError); 281 mWideScreenClearData = mRootView.requireViewById(R.id.car_ui_wideScreenClearData); 282 mFullscreenArea = mRootView.requireViewById(R.id.car_ui_fullscreenArea); 283 mInputExtractEditTextContainer = mRootView.requireViewById( 284 R.id.car_ui_inputExtractEditTextContainer); 285 mWideScreenClearData.setOnClickListener( 286 v -> { 287 // notify the app to clear the data. 288 mInputConnection.performPrivateCommand(WIDE_SCREEN_CLEAR_DATA_ACTION, null); 289 }); 290 mExtractViewHidden = false; 291 292 return mRootView; 293 } 294 295 /** 296 * Compute the interesting insets into your UI. When the content view is shown the default 297 * touchable insets are {@link InputMethodService.Insets#TOUCHABLE_INSETS_FRAME}. When content 298 * view is hidden then that area of the application is interactable by user. 299 * 300 * @param outInsets Fill in with the current UI insets. 301 */ onComputeInsets(@onNull InputMethodService.Insets outInsets)302 public void onComputeInsets(@NonNull InputMethodService.Insets outInsets) { 303 if (!isWideScreenMode()) { 304 return; 305 } 306 Rect tempRect = new Rect(); 307 int[] tempLocation = new int[2]; 308 outInsets.contentTopInsets = outInsets.visibleTopInsets = 309 mInputMethodService.getWindow().getWindow().getDecorView().getHeight(); 310 if (mImeRendersAllContent) { 311 outInsets.touchableRegion.setEmpty(); 312 outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_FRAME; 313 } else { 314 mInputFrame.getLocationOnScreen(tempLocation); 315 tempRect.set(/* left= */0, /* top= */ 0, 316 tempLocation[0] + mInputFrame.getWidth(), 317 tempLocation[1] + mInputFrame.getHeight()); 318 outInsets.touchableRegion.set(tempRect); 319 outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION; 320 } 321 } 322 323 /** 324 * Actions passed by the application must be "automotive_wide_screen" with the corresponding 325 * data 326 * that application wants to display. See the comments associated with each bundle key to know 327 * what view is rendered. 328 * 329 * <p> Each bundle key renders or updates/controls a particular view in the template. For 330 * example, if application rendered the description title and later also wanted to render an 331 * actual description with it then application should use both "add_desc_title_to_content_area" 332 * and "add_desc_to_content_area" to provide the data. Sending action with only 333 * "add_desc_to_content_area" bundle key will not add an extra view but will display only the 334 * description and not the title. 335 * <p> 336 * When the IME window is closed all the views are reset. For the default view visibility see 337 * {@code resetAutomotiveWideScreenViews}. 338 * 339 * @param action Name of the command to be performed. 340 * @param data Any data to include with the command. 341 */ 342 @RequiresApi(TARGET_API_R) onAppPrivateCommand(String action, Bundle data)343 public void onAppPrivateCommand(String action, Bundle data) { 344 if (!isWideScreenMode()) { 345 return; 346 } 347 resetAutomotiveWideScreenViews(); 348 if (data == null) { 349 return; 350 } 351 if (mAllowAppToHideContentArea || (mInputEditorInfo != null && isPackageAuthorized( 352 getEditorInfoPackageName()))) { 353 mImeRendersAllContent = data.getBoolean(REQUEST_RENDER_CONTENT_AREA, true); 354 if (!mImeRendersAllContent) { 355 mContentAreaAutomotive.setVisibility(View.GONE); 356 } else { 357 mContentAreaAutomotive.setVisibility(View.VISIBLE); 358 } 359 } 360 361 if (data.getParcelable(CONTENT_AREA_SURFACE_PACKAGE) != null 362 && Build.VERSION.SDK_INT >= VERSION_CODES.R) { 363 SurfacePackage surfacePackage = (SurfacePackage) data.getParcelable( 364 CONTENT_AREA_SURFACE_PACKAGE); 365 mContentAreaSurfaceView.setChildSurfacePackage(surfacePackage); 366 mContentAreaSurfaceView.setVisibility(View.VISIBLE); 367 mContentAreaAutomotive.setVisibility(View.GONE); 368 } 369 370 String discTitle = data.getString(ADD_DESC_TITLE_TO_CONTENT_AREA); 371 if (!TextUtils.isEmpty(discTitle)) { 372 mWideScreenDescriptionTitle.setText(discTitle); 373 mWideScreenDescriptionTitle.setVisibility(View.VISIBLE); 374 mContentAreaAutomotive.setBackground( 375 mContext.getDrawable(R.drawable.car_ui_ime_wide_screen_background)); 376 } 377 378 String disc = data.getString(ADD_DESC_TO_CONTENT_AREA); 379 if (!TextUtils.isEmpty(disc)) { 380 mWideScreenDescription.setText(disc); 381 mWideScreenDescription.setVisibility(View.VISIBLE); 382 mContentAreaAutomotive.setBackground( 383 mContext.getDrawable(R.drawable.car_ui_ime_wide_screen_background)); 384 } 385 386 String errorMessage = data.getString(ADD_ERROR_DESC_TO_INPUT_AREA); 387 if (!TextUtils.isEmpty(errorMessage)) { 388 mWideScreenErrorMessage.setVisibility(View.VISIBLE); 389 mWideScreenClearData.setVisibility(View.GONE); 390 mWideScreenErrorImage.setVisibility(View.VISIBLE); 391 setExtractedEditTextBackground( 392 R.drawable.car_ui_ime_wide_screen_input_area_tint_error_color); 393 mWideScreenErrorMessage.setText(errorMessage); 394 mContentAreaAutomotive.setBackground( 395 mContext.getDrawable(R.drawable.car_ui_ime_wide_screen_background)); 396 } 397 398 if (TextUtils.isEmpty(errorMessage)) { 399 mWideScreenErrorMessage.setVisibility(View.INVISIBLE); 400 mWideScreenErrorMessage.setText(""); 401 mWideScreenClearData.setVisibility(View.VISIBLE); 402 mWideScreenErrorImage.setVisibility(View.GONE); 403 setExtractedEditTextBackground( 404 R.drawable.car_ui_ime_wide_screen_input_area_tint_color); 405 } 406 407 int extractedTextIcon = data.getInt(WIDE_SCREEN_EXTRACTED_TEXT_ICON_RES_ID); 408 if (extractedTextIcon != 0) { 409 setWideScreenExtractedIcon(extractedTextIcon); 410 } 411 412 byte[] byteArray = data.getByteArray(WIDE_SCREEN_EXTRACTED_TEXT_ICON); 413 if (byteArray != null) { 414 Bitmap bitmap = Bitmap.CREATOR.createFromParcel( 415 byteArrayToParcel(byteArray)); 416 mWideScreenExtractedTextIcon.setImageDrawable( 417 new BitmapDrawable(mContext.getResources(), bitmap)); 418 mWideScreenExtractedTextIcon.setVisibility(View.VISIBLE); 419 } 420 421 if (WIDE_SCREEN_SEARCH_RESULTS.equals(action)) { 422 loadSearchItems(); 423 } 424 425 if (mExtractActionAutomotive != null) { 426 mExtractActionAutomotive.setVisibility(View.VISIBLE); 427 } 428 if (mAutomotiveSearchItems != null) { 429 mRecyclerView.setLayoutManager(new LinearLayoutManager(mContext)); 430 mRecyclerView.setVerticalScrollBarEnabled(true); 431 mRecyclerView.setVerticalScrollbarPosition(View.SCROLLBAR_POSITION_LEFT); 432 mRecyclerView.setVisibility(View.VISIBLE); 433 mRecyclerView.setAdapter(new CarUiListItemAdapter(mAutomotiveSearchItems)); 434 mContentAreaAutomotive.setBackground( 435 mContext.getDrawable(R.drawable.car_ui_ime_wide_screen_background)); 436 if (mExtractActionAutomotive != null) { 437 mExtractActionAutomotive.setVisibility(View.GONE); 438 } 439 } 440 } 441 442 @SuppressLint("Range") loadSearchItems()443 private void loadSearchItems() { 444 if (mInputEditorInfo == null) { 445 Log.w(TAG, "Result can't be loaded, input InputEditorInfo not available "); 446 return; 447 } 448 Uri contentUrl = Uri.parse(SearchResultsProvider.getAuthority( 449 getPackageName(mInputEditorInfo))); 450 ContentResolver cr = mContext.getContentResolver(); 451 try (Cursor c = cr.query(contentUrl, null, null, null, null)) { 452 mAutomotiveSearchItems = new ArrayList<>(); 453 if (c != null && c.moveToFirst()) { 454 do { 455 CarUiContentListItem searchItem = new CarUiContentListItem( 456 CarUiContentListItem.Action.ICON); 457 String itemId = c.getString(c.getColumnIndex(SearchResultsProvider.ITEM_ID)); 458 searchItem.setOnItemClickedListener(v -> onItemClicked(itemId)); 459 searchItem.setTitle(c.getString( 460 c.getColumnIndex(SearchResultsProvider.TITLE))); 461 searchItem.setBody(c.getString( 462 c.getColumnIndex(SearchResultsProvider.SUBTITLE))); 463 searchItem.setPrimaryIconType(CarUiContentListItem.IconType.CONTENT); 464 byte[] primaryBlob = c.getBlob( 465 c.getColumnIndex( 466 SearchResultsProvider.PRIMARY_IMAGE_BLOB)); 467 if (primaryBlob != null) { 468 Bitmap primaryBitmap = Bitmap.CREATOR.createFromParcel( 469 byteArrayToParcel(primaryBlob)); 470 searchItem.setIcon( 471 new BitmapDrawable(mContext.getResources(), primaryBitmap)); 472 } 473 byte[] secondaryBlob = c.getBlob( 474 c.getColumnIndex( 475 SearchResultsProvider.SECONDARY_IMAGE_BLOB)); 476 477 if (secondaryBlob != null) { 478 Bitmap secondaryBitmap = Bitmap.CREATOR.createFromParcel( 479 byteArrayToParcel(secondaryBlob)); 480 String secondaryItemId = c.getString(c.getColumnIndex( 481 SearchResultsProvider.SECONDARY_IMAGE_ID)); 482 searchItem.setSupplementalIcon( 483 new BitmapDrawable(mContext.getResources(), secondaryBitmap), v -> { 484 Bundle bundle = new Bundle(); 485 bundle.putString(SEARCH_RESULT_SUPPLEMENTAL_ICON_ID_LIST, 486 secondaryItemId); 487 mInputConnection.performPrivateCommand(WIDE_SCREEN_ACTION, 488 bundle); 489 }); 490 } 491 mAutomotiveSearchItems.add(searchItem); 492 } while (c.moveToNext()); 493 } 494 } 495 496 mInputConnection.performPrivateCommand(WIDE_SCREEN_POST_LOAD_SEARCH_RESULTS_ACTION, null); 497 } 498 onItemClicked(String itemId)499 void onItemClicked(String itemId) { 500 Bundle bundle = new Bundle(); 501 bundle.putString(SEARCH_RESULT_ITEM_ID_LIST, itemId); 502 mInputConnection.performPrivateCommand(WIDE_SCREEN_ACTION, bundle); 503 } 504 byteArrayToParcel(byte[] bytes)505 private static Parcel byteArrayToParcel(byte[] bytes) { 506 Parcel parcel = Parcel.obtain(); 507 parcel.unmarshall(bytes, 0, bytes.length); 508 parcel.setDataPosition(0); 509 return parcel; 510 } 511 512 /** 513 * Evaluate if IME should launch in a fullscreen mode. In wide screen mode IME should always 514 * launch in a fullscreen mode so that {@link ExtractEditText} is inflated. Later the controller 515 * will detach the {@link ExtractEditText} from its original parent and inflate into the 516 * appropriate container in wide screen. 517 * 518 * @param isFullScreen value evaluated to be in fullscreen mode or not by the app. 519 */ onEvaluateFullscreenMode(boolean isFullScreen)520 public boolean onEvaluateFullscreenMode(boolean isFullScreen) { 521 return isWideScreenMode() || isFullScreen; 522 } 523 524 /** 525 * Initialize the view in the wide screen template based on the data provided by the app through 526 * {@link #onAppPrivateCommand(String, Bundle)} 527 */ 528 @RequiresApi(TARGET_API_R) onStartInputView(@onNull EditorInfo editorInfo, @Nullable InputConnection inputConnection, @Nullable CharSequence textForImeAction)529 public void onStartInputView(@NonNull EditorInfo editorInfo, 530 @Nullable InputConnection inputConnection, 531 @Nullable CharSequence textForImeAction) { 532 if (!isWideScreenMode()) { 533 return; 534 } 535 mInputEditorInfo = editorInfo; 536 mInputConnection = inputConnection; 537 View header = mRootView.requireViewById(R.id.car_ui_imeWideScreenInputArea); 538 539 header.setVisibility(View.VISIBLE); 540 if (mExtractViewHidden) { 541 mFullscreenArea.setVisibility(View.INVISIBLE); 542 } else { 543 mFullscreenArea.setVisibility(View.VISIBLE); 544 } 545 546 // This view is rendered by the framework when IME is in full screen mode. For more info 547 // see {@link #onEvaluateFullscreenMode} 548 mExtractEditText = getExtractEditText(); 549 mExtractEditText.setPadding( 550 mContext.getResources().getDimensionPixelSize( 551 R.dimen.car_ui_ime_wide_screen_input_edit_text_padding_left), 552 /* top= */0, 553 mContext.getResources().getDimensionPixelSize( 554 R.dimen.car_ui_ime_wide_screen_input_edit_text_padding_right), 555 /* bottom= */0); 556 mExtractEditText.setTextSize(mContext.getResources().getDimensionPixelSize( 557 R.dimen.car_ui_ime_wide_screen_input_edit_text_size)); 558 mExtractEditText.setGravity(Gravity.START | Gravity.CENTER); 559 560 ViewGroup parent = (ViewGroup) mExtractEditText.getParent(); 561 parent.removeViewInLayout(mExtractEditText); 562 563 FrameLayout.LayoutParams params = 564 new FrameLayout.LayoutParams( 565 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); 566 567 mInputExtractEditTextContainer.addView(mExtractEditText, params); 568 569 ImageView close = mRootView.findViewById(R.id.car_ui_closeKeyboard); 570 if (close != null) { 571 close.setOnClickListener( 572 (v) -> { 573 mInputMethodService.requestHideSelf(0); 574 mInputConnection.performPrivateCommand(WIDE_SCREEN_ON_BACK_CLICKED_ACTION, 575 null); 576 }); 577 } 578 579 if (!mIsExtractIconProvidedByApp) { 580 setWideScreenExtractedIcon(/* iconResId= */0); 581 } 582 583 boolean hasAction = (mInputEditorInfo.imeOptions & EditorInfo.IME_MASK_ACTION) 584 != EditorInfo.IME_ACTION_NONE; 585 boolean hasInputType = mInputEditorInfo.inputType != InputType.TYPE_NULL; 586 boolean hasNoAccessoryAction = 587 (mInputEditorInfo.imeOptions & EditorInfo.IME_FLAG_NO_ACCESSORY_ACTION) == 0; 588 589 boolean hasLabel = 590 mInputEditorInfo.actionLabel != null || (hasAction && hasNoAccessoryAction 591 && hasInputType); 592 593 if (hasLabel) { 594 intiExtractAction(textForImeAction); 595 } 596 597 if (mContentAreaSurfaceView.getVisibility() == View.GONE) { 598 sendSurfaceInfo(); 599 } 600 } 601 602 @VisibleForTesting getExtractEditText()603 ExtractEditText getExtractEditText() { 604 return mRootView.getRootView().requireViewById( 605 android.R.id.inputExtractEditText); 606 } 607 608 /** 609 * Sends the information for surface view to the application on which they can draw on. This 610 * information will ONLY be sent if OEM allows an application to hide the content area and let 611 * it draw its own content. 612 */ 613 @RequiresApi(TARGET_API_R) sendSurfaceInfo()614 private void sendSurfaceInfo() { 615 if (!mAllowAppToHideContentArea && mContentAreaSurfaceView.getDisplay() == null 616 && !(mInputEditorInfo != null 617 && isPackageAuthorized(getEditorInfoPackageName()))) { 618 return; 619 } 620 // Dispatch the window visibility change for IME window as soon as its displayed. 621 mRootView.dispatchWindowVisibilityChanged(View.VISIBLE); 622 int displayId = mContentAreaSurfaceView.getDisplay() == null 623 ? 0 : mContentAreaSurfaceView.getDisplay().getDisplayId(); 624 IBinder hostToken = mContentAreaSurfaceView.getHostToken(); 625 626 Bundle bundle = new Bundle(); 627 bundle.putBinder(CONTENT_AREA_SURFACE_HOST_TOKEN, hostToken); 628 bundle.putInt(CONTENT_AREA_SURFACE_DISPLAY_ID, displayId); 629 bundle.putInt(CONTENT_AREA_SURFACE_HEIGHT, 630 mContentAreaSurfaceView.getHeight() + getNavBarHeight()); 631 bundle.putInt(CONTENT_AREA_SURFACE_WIDTH, mContentAreaSurfaceView.getWidth()); 632 633 mInputConnection.performPrivateCommand(WIDE_SCREEN_ACTION, bundle); 634 } 635 636 @VisibleForTesting isPackageAuthorized(String packageName)637 boolean isPackageAuthorized(String packageName) { 638 String[] packages = mContext.getResources() 639 .getStringArray(R.array.car_ui_ime_wide_screen_allowed_package_list); 640 641 PackageInfo packageInfo = getPackageInfo(mContext, packageName); 642 // Checks if the application of the given context is installed in the system image. I.e. 643 // if it's a bundled app. 644 if (packageInfo != null && (packageInfo.applicationInfo.flags & (ApplicationInfo.FLAG_SYSTEM 645 | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) != 0) { 646 return true; 647 } 648 649 for (String pattern : packages) { 650 String regex = createRegexFromGlob(pattern); 651 if (packageName.matches(regex)) { 652 return true; 653 } 654 } 655 return false; 656 } 657 658 /** 659 * Return the package info for a particular package. 660 */ 661 @Nullable getPackageInfo(Context context, String packageName)662 private static PackageInfo getPackageInfo(Context context, 663 String packageName) { 664 PackageManager packageManager = context.getPackageManager(); 665 PackageInfo packageInfo = null; 666 try { 667 packageInfo = packageManager.getPackageInfo( 668 packageName, /* flags= */ 0); 669 } catch (PackageManager.NameNotFoundException ex) { 670 Log.e(TAG, "package not found: " + packageName); 671 } 672 return packageInfo; 673 } 674 createRegexFromGlob(String glob)675 private static String createRegexFromGlob(String glob) { 676 Pattern reg = Pattern.compile(NOT_ASTERISK_OR_CAPTURED_ASTERISK); 677 Matcher m = reg.matcher(glob); 678 StringBuffer b = new StringBuffer(); 679 while (m.find()) { 680 if (m.group(1) != null) { 681 m.appendReplacement(b, ".*"); 682 } else { 683 m.appendReplacement(b, Matcher.quoteReplacement(m.group(0))); 684 } 685 } 686 m.appendTail(b); 687 return b.toString(); 688 } 689 getNavBarHeight()690 private int getNavBarHeight() { 691 Resources resources = mContext.getResources(); 692 int resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android"); 693 if (resourceId > 0) { 694 return resources.getDimensionPixelSize(resourceId); 695 } 696 return 0; 697 } 698 699 /** 700 * To support wide screen mode, IME should always call 701 * {@link InputMethodService#setExtractViewShown} 702 * with false and pass the flag to this method. 703 * <p> 704 * For example, within the IMS service call 705 * <pre> 706 * @Override 707 * public void setExtractViewShown(boolean shown) { 708 * if (!carUiImeWideScreenController.isWideScreenMode()) { 709 * super.setExtractViewShown(shown); 710 * return; 711 * } 712 * super.setExtractViewShown(false); 713 * mImeWideScreenController.setExtractViewShown(shown); 714 * } 715 * </pre> 716 * <p> 717 * This is required as IMS checks for ExtractViewIsShown and if that is true then set the 718 * touchable insets to the entire screen rather than a region. If an app hides the content area 719 * in that case we want the user to be able to interact with the application. 720 */ setExtractViewShown(boolean shown)721 public void setExtractViewShown(boolean shown) { 722 if (!isWideScreenMode()) { 723 return; 724 } 725 if (mExtractViewHidden == !shown) { 726 return; 727 } 728 mExtractViewHidden = !shown; 729 if (mExtractViewHidden) { 730 mFullscreenArea.setVisibility(View.INVISIBLE); 731 } else { 732 mFullscreenArea.setVisibility(View.VISIBLE); 733 } 734 } 735 intiExtractAction(CharSequence textForImeAction)736 private void intiExtractAction(CharSequence textForImeAction) { 737 if (mExtractActionAutomotive == null) { 738 return; 739 } 740 if (mInputEditorInfo.actionLabel != null) { 741 ((TextView) mExtractActionAutomotive).setText(mInputEditorInfo.actionLabel); 742 } else { 743 ((TextView) mExtractActionAutomotive).setText(textForImeAction); 744 } 745 746 // click listener for the action button shown in the content area. 747 mExtractActionAutomotive.setOnClickListener(v -> { 748 final EditorInfo editorInfo = mInputEditorInfo; 749 final InputConnection inputConnection = mInputConnection; 750 if (editorInfo == null || inputConnection == null) { 751 return; 752 } 753 if (editorInfo.actionId != 0) { 754 inputConnection.performEditorAction(editorInfo.actionId); 755 } else if ((editorInfo.imeOptions & EditorInfo.IME_MASK_ACTION) 756 != EditorInfo.IME_ACTION_NONE) { 757 inputConnection.performEditorAction( 758 editorInfo.imeOptions & EditorInfo.IME_MASK_ACTION); 759 } 760 }); 761 } 762 setExtractedEditTextBackground(int drawableResId)763 private void setExtractedEditTextBackground(int drawableResId) { 764 mExtractEditText.setBackgroundTintList(mContext.getColorStateList(drawableResId)); 765 } 766 767 @VisibleForTesting setContentAreaSurfaceView(SurfaceView surfaceView)768 void setContentAreaSurfaceView(SurfaceView surfaceView) { 769 mContentAreaSurfaceView = surfaceView; 770 } 771 772 @VisibleForTesting getPackageName(EditorInfo editorInfo)773 String getPackageName(EditorInfo editorInfo) { 774 return editorInfo.packageName; 775 } 776 777 /** 778 * Sets the icon in the input area. If the icon resource Id is not provided by the application 779 * then application icon will be used instead. 780 * 781 * @param iconResId icon resource id for the image drawable to load. 782 */ setWideScreenExtractedIcon(@rawableRes int iconResId)783 private void setWideScreenExtractedIcon(@DrawableRes int iconResId) { 784 if (mInputEditorInfo == null || mWideScreenExtractedTextIcon == null) { 785 return; 786 } 787 try { 788 if (iconResId == 0) { 789 mWideScreenExtractedTextIcon.setImageDrawable( 790 mContext.getPackageManager().getApplicationIcon( 791 getEditorInfoPackageName())); 792 } else { 793 mIsExtractIconProvidedByApp = true; 794 mWideScreenExtractedTextIcon.setImageDrawable( 795 mContext.createPackageContext(getEditorInfoPackageName(), 0).getDrawable( 796 iconResId)); 797 } 798 mWideScreenExtractedTextIcon.setVisibility(View.VISIBLE); 799 } catch (PackageManager.NameNotFoundException ex) { 800 Log.w(TAG, "setWideScreenExtractedIcon: package name not found ", ex); 801 mWideScreenExtractedTextIcon.setVisibility(View.GONE); 802 } catch (Resources.NotFoundException ex) { 803 Log.w(TAG, "setWideScreenExtractedIcon: resource not found with id " + iconResId, ex); 804 mWideScreenExtractedTextIcon.setVisibility(View.GONE); 805 } 806 } 807 808 @VisibleForTesting getEditorInfoPackageName()809 String getEditorInfoPackageName() { 810 return mInputEditorInfo != null ? mInputEditorInfo.packageName : null; 811 } 812 813 /** 814 * Called when IME window closes. Reset all the views once that happens. 815 */ 816 @RequiresApi(TARGET_API_R) onFinishInputView()817 public void onFinishInputView() { 818 if (!isWideScreenMode()) { 819 return; 820 } 821 resetAutomotiveWideScreenViews(); 822 } 823 824 @RequiresApi(TARGET_API_R) resetAutomotiveWideScreenViews()825 private void resetAutomotiveWideScreenViews() { 826 mWideScreenDescriptionTitle.setVisibility(View.GONE); 827 mContentAreaSurfaceView.setVisibility(View.GONE); 828 mContentAreaSurfaceView.setChildSurfacePackage(null); 829 mWideScreenErrorMessage.setVisibility(View.GONE); 830 mRecyclerView.setVisibility(View.GONE); 831 mWideScreenDescription.setVisibility(View.GONE); 832 mFullscreenArea.setVisibility(View.VISIBLE); 833 if (mWideScreenExtractedTextIcon != null) { 834 mWideScreenExtractedTextIcon.setVisibility(View.VISIBLE); 835 } 836 mWideScreenClearData.setVisibility(View.VISIBLE); 837 mWideScreenErrorImage.setVisibility(View.GONE); 838 if (mExtractActionAutomotive != null) { 839 mExtractActionAutomotive.setVisibility(View.GONE); 840 } 841 mContentAreaAutomotive.setVisibility(View.VISIBLE); 842 mContentAreaAutomotive.setBackground( 843 mContext.getDrawable(R.drawable.car_ui_ime_wide_screen_no_content_background)); 844 setExtractedEditTextBackground(R.drawable.car_ui_ime_wide_screen_input_area_tint_color); 845 mImeRendersAllContent = true; 846 mIsExtractIconProvidedByApp = false; 847 mExtractViewHidden = false; 848 mAutomotiveSearchItems = null; 849 } 850 851 /** 852 * Returns whether or not system is running in a wide screen mode. 853 */ isWideScreenMode()854 public boolean isWideScreenMode() { 855 return CarUiUtils.getBooleanSystemProperty(mContext.getResources(), 856 R.string.car_ui_ime_wide_screen_system_property_name, false) 857 && Build.VERSION.SDK_INT >= VERSION_CODES.R; 858 } 859 } 860