• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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