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      *   &#64;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