| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.assist.service; |
| |
| import static android.view.WindowInsets.Type.displayCutout; |
| import static android.view.WindowInsets.Type.statusBars; |
| |
| import android.app.assist.AssistContent; |
| import android.app.assist.AssistStructure; |
| import android.assist.common.Utils; |
| import android.content.BroadcastReceiver; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.graphics.Bitmap; |
| import android.graphics.Color; |
| import android.graphics.Point; |
| import android.graphics.Rect; |
| import android.os.Bundle; |
| import android.os.RemoteCallback; |
| import android.service.voice.VoiceInteractionSession; |
| import android.util.Log; |
| import android.view.Display; |
| import android.view.DisplayCutout; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewTreeObserver; |
| import android.view.WindowInsets; |
| import android.view.WindowManager; |
| import android.view.WindowMetrics; |
| |
| public class MainInteractionSession extends VoiceInteractionSession { |
| static final String TAG = "MainInteractionSession"; |
| |
| Context mContext; |
| Bundle mAssistData = new Bundle(); |
| |
| private boolean hasReceivedAssistData = false; |
| private boolean hasReceivedScreenshot = false; |
| private boolean mScreenshotNeeded = true; |
| private int mCurColor; |
| private int mDisplayHeight; |
| private int mDisplayWidth; |
| private Rect mDisplayAreaBounds; |
| private BroadcastReceiver mReceiver; |
| private String mTestName; |
| private View mContentView; |
| private RemoteCallback mRemoteCallback; |
| |
| MainInteractionSession(Context context) { |
| super(context); |
| mContext = context; |
| } |
| |
| @Override |
| public void onCreate() { |
| super.onCreate(); |
| mReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| String action = intent.getAction(); |
| if (action.equals(Utils.HIDE_SESSION)) { |
| hide(); |
| } |
| |
| Bundle bundle = new Bundle(); |
| bundle.putString(Utils.EXTRA_REMOTE_CALLBACK_ACTION, Utils.HIDE_SESSION_COMPLETE); |
| mRemoteCallback.sendResult(bundle); |
| } |
| }; |
| IntentFilter filter = new IntentFilter(); |
| filter.addAction(Utils.HIDE_SESSION); |
| mContext.registerReceiver(mReceiver, filter, Context.RECEIVER_VISIBLE_TO_INSTANT_APPS); |
| } |
| |
| @Override |
| public void onDestroy() { |
| Log.i(TAG, "onDestroy()"); |
| super.onDestroy(); |
| if (mReceiver != null) { |
| try { |
| mContext.unregisterReceiver(mReceiver); |
| } catch (IllegalArgumentException e) { |
| // Ignore this exception when unregisterReceiver fails. Due to there will be timing |
| // case to destroy VoiceInteractionSessionService before VoiceInteractionSession. |
| Log.e(TAG, "Failed to unregister receiver in onDestroy.", e); |
| } |
| } |
| } |
| |
| @Override |
| public void onPrepareShow(Bundle args, int showFlags) { |
| if (Utils.LIFECYCLE_NOUI.equals(args.getString(Utils.TESTCASE_TYPE, ""))) { |
| setUiEnabled(false); |
| } else { |
| setUiEnabled(true); |
| } |
| } |
| |
| @Override |
| public void onShow(Bundle args, int showFlags) { |
| if (args == null) { |
| Log.e(TAG, "onshow() received null args"); |
| return; |
| } |
| mScreenshotNeeded = (showFlags & SHOW_WITH_SCREENSHOT) != 0; |
| mTestName = args.getString(Utils.TESTCASE_TYPE, ""); |
| mCurColor = args.getInt(Utils.SCREENSHOT_COLOR_KEY); |
| mDisplayHeight = args.getInt(Utils.DISPLAY_HEIGHT_KEY); |
| mDisplayWidth = args.getInt(Utils.DISPLAY_WIDTH_KEY); |
| mDisplayAreaBounds = args.getParcelable(Utils.DISPLAY_AREA_BOUNDS_KEY); |
| mRemoteCallback = args.getParcelable(Utils.EXTRA_REMOTE_CALLBACK); |
| super.onShow(args, showFlags); |
| if (mContentView == null) return; // Happens when ui is not enabled. |
| mContentView.getViewTreeObserver().addOnPreDrawListener( |
| new ViewTreeObserver.OnPreDrawListener() { |
| @Override |
| public boolean onPreDraw() { |
| mContentView.getViewTreeObserver().removeOnPreDrawListener(this); |
| Display d = mContentView.getDisplay(); |
| Point displayPoint = new Point(); |
| // The voice interaction window layer is higher than keyguard, status bar, |
| // nav bar now. So we should take both status bar, nav bar into consideration. |
| // The voice interaction hide the nav bar, so the height only need to consider |
| // status bar. The status bar may contain display cutout but the display cutout |
| // is device specific, we need to check it. |
| WindowManager wm = mContext.getSystemService(WindowManager.class); |
| WindowMetrics windowMetrics = wm.getCurrentWindowMetrics(); |
| Rect bound = windowMetrics.getBounds(); |
| WindowInsets windowInsets = windowMetrics.getWindowInsets(); |
| android.graphics.Insets statusBarInsets = |
| windowInsets.getInsets(statusBars()); |
| android.graphics.Insets displayCutoutInsets = |
| windowInsets.getInsets(displayCutout()); |
| android.graphics.Insets min = |
| android.graphics.Insets.min(statusBarInsets, displayCutoutInsets); |
| boolean statusBarContainsCutout = !android.graphics.Insets.NONE.equals(min); |
| Log.d(TAG, "statusBarContainsCutout=" + statusBarContainsCutout); |
| displayPoint.y = statusBarContainsCutout |
| ? bound.height() - min.top - min.bottom : bound.height(); |
| displayPoint.x = bound.width(); |
| DisplayCutout dc = d.getCutout(); |
| if (dc != null) { |
| // Means the device has a cutout area |
| android.graphics.Insets wi = d.getCutout().getWaterfallInsets(); |
| |
| if (wi != android.graphics.Insets.NONE) { |
| // Waterfall cutout. Considers only the display |
| // useful area discarding the cutout. |
| displayPoint.x -= (wi.left + wi.right); |
| } |
| } |
| Bundle bundle = new Bundle(); |
| bundle.putString(Utils.EXTRA_REMOTE_CALLBACK_ACTION, |
| Utils.BROADCAST_CONTENT_VIEW_HEIGHT); |
| bundle.putInt(Utils.EXTRA_CONTENT_VIEW_HEIGHT, mContentView.getHeight()); |
| bundle.putInt(Utils.EXTRA_CONTENT_VIEW_WIDTH, mContentView.getWidth()); |
| bundle.putParcelable(Utils.EXTRA_DISPLAY_POINT, displayPoint); |
| mRemoteCallback.sendResult(bundle); |
| return true; |
| } |
| }); |
| } |
| |
| @Override |
| public void onHandleAssist(AssistState state) { |
| super.onHandleAssist(state); |
| Bundle data = state.getAssistData(); |
| AssistStructure structure = state.getAssistStructure(); |
| AssistContent content = state.getAssistContent(); |
| ComponentName activity = structure == null ? null : structure.getActivityComponent(); |
| Log.i(TAG, "onHandleAssist()"); |
| Log.i(TAG, String.format("Bundle: %s, Activity: %s, Structure: %s, Content: %s", |
| data, activity, structure, content)); |
| |
| if (activity != null && Utils.isAutomotive(mContext) |
| && !activity.getPackageName().startsWith("android.assist")) { |
| // TODO: automotive has multiple activities / displays, so the test might fail if it |
| // receives one of them (like the cluster activity) instead of what's expecting. This is |
| // a quick fix for the issue; a better solution would be refactoring the infra to |
| // either send all events, or let the test specifify which activity it's waiting for |
| Log.i(TAG, "Ignoring " + activity.flattenToShortString() + " on automotive"); |
| return; |
| } |
| |
| if (structure != null && structure.isHomeActivity() && !state.isFocused()) { |
| // If the system has multiple display areas, the launcher may be visible and resumed |
| // when the tests are in progress, so the tests might fail if they receives unexpected |
| // state from the launcher. Ignore the states from unfocused launcher to avoid this |
| // failure. |
| Log.i(TAG, "Ignoring the state from unfocused launcher"); |
| return; |
| } |
| |
| // send to test to verify that this is accurate. |
| mAssistData.putBoolean(Utils.ASSIST_IS_ACTIVITY_ID_NULL, state.getActivityId() == null); |
| mAssistData.putParcelable(Utils.ASSIST_STRUCTURE_KEY, structure); |
| mAssistData.putParcelable(Utils.ASSIST_CONTENT_KEY, content); |
| mAssistData.putBundle(Utils.ASSIST_BUNDLE_KEY, data); |
| hasReceivedAssistData = true; |
| maybeBroadcastResults(); |
| } |
| |
| @Override |
| public void onAssistStructureFailure(Throwable failure) { |
| Log.e(TAG, "onAssistStructureFailure(): D'OH!!!", failure); |
| } |
| |
| @Override |
| public void onHandleScreenshot(/*@Nullable*/ Bitmap screenshot) { |
| Log.i(TAG, String.format("onHandleScreenshot - Screenshot: %s", screenshot)); |
| super.onHandleScreenshot(screenshot); |
| |
| if (screenshot != null) { |
| mAssistData.putBoolean(Utils.ASSIST_SCREENSHOT_KEY, true); |
| |
| if (mTestName.equals(Utils.SCREENSHOT)) { |
| boolean screenshotMatches = compareScreenshot(screenshot, mCurColor); |
| Log.i(TAG, "this is a screenshot test. Matches? " + screenshotMatches); |
| mAssistData.putBoolean( |
| Utils.COMPARE_SCREENSHOT_KEY, screenshotMatches); |
| } |
| } else { |
| mAssistData.putBoolean(Utils.ASSIST_SCREENSHOT_KEY, false); |
| } |
| hasReceivedScreenshot = true; |
| maybeBroadcastResults(); |
| } |
| |
| private boolean compareScreenshot(Bitmap screenshot, int color) { |
| // TODO(b/215668037): Uncomment when we find a reliable approach across different form |
| // factors. |
| // The current approach does not handle overridden screen sizes, and there's no clear way |
| // to handle that and multiple display areas at the same time. |
| // Point size = new Point(mDisplayWidth, mDisplayHeight); |
| |
| // if (screenshot.getWidth() != size.x || screenshot.getHeight() != size.y) { |
| // Log.i(TAG, "width or height didn't match: " + size + " vs " + screenshot.getWidth() |
| // + "," + screenshot.getHeight()); |
| // return false; |
| // } |
| Point size = new Point(screenshot.getWidth(), screenshot.getHeight()); |
| int[] pixels = new int[size.x * size.y]; |
| screenshot.getPixels(pixels, 0, size.x, 0, 0, size.x, size.y); |
| |
| // screenshot bitmap contains the screenshot for the entire physical display. A single |
| // physical display could have multiple display area with different applications. |
| // Let's grab the region of the display area from the original screenshot. |
| Bitmap displayAreaScreenshot = Bitmap.createBitmap(screenshot, mDisplayAreaBounds.left, |
| mDisplayAreaBounds.top, mDisplayAreaBounds.width(), mDisplayAreaBounds.height()); |
| int expectedColor = 0; |
| for (int pixel : pixels) { |
| // Check for roughly the same because there are rounding errors converting from the |
| // screenshot's color space to SRGB, which is what getPixels does. |
| if ((Color.red(pixel) - Color.red(color) < 5) |
| && (Color.green(pixel) - Color.green(color) < 5) |
| && (Color.blue(pixel) - Color.blue(color) < 5)) { |
| expectedColor += 1; |
| } |
| } |
| |
| int pixelCount = displayAreaScreenshot.getWidth() * displayAreaScreenshot.getHeight(); |
| double colorRatio = (double) expectedColor / pixelCount; |
| Log.i(TAG, "the ratio is " + colorRatio); |
| return colorRatio >= 0.6; |
| } |
| |
| private void maybeBroadcastResults() { |
| if (!hasReceivedAssistData) { |
| Log.i(TAG, "waiting for assist data before broadcasting results"); |
| } else if (mScreenshotNeeded && !hasReceivedScreenshot) { |
| Log.i(TAG, "waiting for screenshot before broadcasting results"); |
| } else { |
| Bundle bundle = new Bundle(); |
| bundle.putString(Utils.EXTRA_REMOTE_CALLBACK_ACTION, Utils.BROADCAST_ASSIST_DATA_INTENT); |
| bundle.putAll(mAssistData); |
| mRemoteCallback.sendResult(bundle); |
| |
| hasReceivedAssistData = false; |
| hasReceivedScreenshot = false; |
| } |
| } |
| |
| @Override |
| public View onCreateContentView() { |
| LayoutInflater f = getLayoutInflater(); |
| if (f == null) { |
| Log.wtf(TAG, "layout inflater was null"); |
| } |
| mContentView = f.inflate(R.layout.assist_layer,null); |
| Log.i(TAG, "onCreateContentView"); |
| return mContentView; |
| } |
| } |