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