/* * Copyright (C) 2015 Google Inc. * * 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 com.googlecode.eyesfree.testing; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.os.SystemClock; import android.support.annotation.IntDef; import android.util.Log; import android.view.KeyEvent; import android.view.View; import android.view.accessibility.AccessibilityEvent; import com.android.talkback.BuildConfig; import com.android.talkback.FeedbackItem; import com.android.talkback.KeyComboManager; import com.android.talkback.SpeechController.SpeechControllerListener; import com.android.utils.SharedPreferencesUtils; import com.google.android.marvin.talkback.TalkBackService; import com.android.talkback.Utterance; import com.android.talkback.eventprocessor.AccessibilityEventProcessor.TalkBackListener; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; public class TalkBackInstrumentationTestCase extends BaseAccessibilityInstrumentationTestCase { @IntDef({RECORD_NONE, RECORD_QUEUED, RECORD_STARTED}) @Retention(RetentionPolicy.SOURCE) public @interface RecordingMode {} public static final int RECORD_NONE = 0; public static final int RECORD_QUEUED = 1; public static final int RECORD_STARTED = 2; private static final long KEY_EVENT_DOWN_TIME = 0; private static final long KEY_EVENT_EVENT_TIME = 0; private static final String TARGET_PACKAGE = BuildConfig.APPLICATION_ID; private static final String TARGET_CLASS = "com.google.android.marvin.talkback.TalkBackService"; /** Maximum time to wait for a specific utterance. */ private static final long OBTAIN_UTTERANCE_TIMEOUT = 5000; /** List of recorded utterances. */ private final ArrayList<Utterance> mUtteranceCache = new ArrayList<>(); /** List of recorded raw speech feedback items. */ private final ArrayList<FeedbackItem> mRawSpeechCache = new ArrayList<>(); /** Whether we're currently recording utterances. */ private boolean mRecordingUtterances; /** Whether we're currently recording raw speech feedback items, and at what time. */ private @RecordingMode int mRecordingRawSpeech; private TalkBackService mService; @Override protected TalkBackService getService() { mService = TalkBackService.getInstance(); return mService; } @Override protected void enableTargetService() { assertServiceIsInstalled(TARGET_PACKAGE, TARGET_CLASS); // Prevent TalkBack from automatically opening the tutorial. final SharedPreferences prefs = SharedPreferencesUtils.getSharedPreferences(mAppCtx); final Editor editor = prefs.edit(); editor.putBoolean("first_time_user", false); editor.commit(); enableService(TARGET_PACKAGE, TARGET_CLASS, true /* usesExploreByTouch */); } @Override protected void connectServiceListener() { mService.setTestingListener(mTestingListener); mService.getSpeechController().setSpeechListener(mTestingSpeechListener); } @Override protected void disconnectServiceListener() { mService.setTestingListener(null); mService.getSpeechController().setSpeechListener(null); } protected void startRecordingUtterances() { synchronized (mUtteranceCache) { mUtteranceCache.clear(); mRecordingUtterances = true; } } protected void startRecordingRawSpeech() { startRecordingRawSpeech(RECORD_QUEUED); } protected void startRecordingRawSpeech(@RecordingMode int mode) { synchronized (mRawSpeechCache) { mRawSpeechCache.clear(); mRecordingRawSpeech = mode; } } protected Utterance stopRecordingUtterancesAfterMatch(UtteranceFilter filter) { final long startTime = SystemClock.uptimeMillis(); synchronized (mUtteranceCache) { try { int currentIndex = 0; while (true) { // Check all events starting from the current index. for (; currentIndex < mUtteranceCache.size(); currentIndex++) { final Utterance utterance = mUtteranceCache.get(currentIndex); if (filter.matches(utterance)) { mRecordingUtterances = false; return utterance; } } final long elapsed = (SystemClock.uptimeMillis() - startTime); final long timeLeft = (OBTAIN_UTTERANCE_TIMEOUT - elapsed); if (timeLeft <= 0) { break; } mUtteranceCache.wait(timeLeft); } mRecordingUtterances = false; } catch (InterruptedException e) { // Do nothing. } } return null; } protected FeedbackItem stopRecordingRawSpeechAfterMatch(FeedbackItemFilter filter) { final long startTime = SystemClock.uptimeMillis(); synchronized (mRawSpeechCache) { try { int currentIndex = 0; while (true) { // Check all events starting from the current index. for (; currentIndex < mRawSpeechCache.size(); currentIndex++) { final FeedbackItem feedbackItem = mRawSpeechCache.get(currentIndex); if (filter.matches(feedbackItem)) { mRecordingRawSpeech = RECORD_NONE; return feedbackItem; } } final long elapsed = (SystemClock.uptimeMillis() - startTime); final long timeLeft = (OBTAIN_UTTERANCE_TIMEOUT - elapsed); if (timeLeft <= 0) { break; } mRawSpeechCache.wait(timeLeft); } mRecordingRawSpeech = RECORD_NONE; } catch (InterruptedException e) { // Do nothing. } } return null; } protected void stopRecordingAndAssertUtterance(String utterance) { CharSequenceFilter textFilter = new CharSequenceFilter().addMatchesPattern(utterance, 0); UtteranceFilter utteranceFilter = new UtteranceFilter().addTextFilter(textFilter); Utterance result = stopRecordingUtterancesAfterMatch(utteranceFilter); assertNotNull(result); } protected void stopRecordingAndAssertRawSpeech(String feedback) { CharSequenceFilter textFilter = new CharSequenceFilter().addMatchesPattern(feedback, 0); FeedbackItemFilter feedbackFilter = new FeedbackItemFilter().addTextFilter(textFilter); FeedbackItem result = stopRecordingRawSpeechAfterMatch(feedbackFilter); assertNotNull(result); } protected List<Utterance> getUtteranceHistory() { synchronized (mUtteranceCache) { return new ArrayList<Utterance>(mUtteranceCache); } } protected List<FeedbackItem> getRawSpeechHistory() { synchronized (mRawSpeechCache) { return new ArrayList<>(mRawSpeechCache); } } protected void afterEventReceived(AccessibilityEvent event) {} private final TalkBackListener mTestingListener = new TalkBackListener() { @Override public void onAccessibilityEvent(AccessibilityEvent event) { onEventReceived(event); } @Override public void afterAccessibilityEvent(AccessibilityEvent event) { afterEventReceived(event); } // Note: This method is called on the TalkBack service thread. @Override public void onUtteranceQueued(Utterance utterance) { synchronized (mUtteranceCache) { if (mRecordingUtterances) { mUtteranceCache.add(utterance); mUtteranceCache.notifyAll(); } } } }; private final SpeechControllerListener mTestingSpeechListener = new SpeechControllerListener() { @Override public void onUtteranceQueued(FeedbackItem utterance) { synchronized (mRawSpeechCache) { if (mRecordingRawSpeech == RECORD_QUEUED) { mRawSpeechCache.add(utterance); mRawSpeechCache.notifyAll(); } } } @Override public void onUtteranceStarted(FeedbackItem utterance) { synchronized (mRawSpeechCache) { if (mRecordingRawSpeech == RECORD_STARTED) { mRawSpeechCache.add(utterance); mRawSpeechCache.notifyAll(); } } } @Override public void onUtteranceCompleted(int utteranceIndex, int status) {} }; /** * Sends down and up key event. */ protected void sendKeyEventDownAndUp(int modifier, int keyCode, KeyComboManager keyComboManager) { sendKeyEvent(KeyEvent.ACTION_DOWN, modifier, keyCode, keyComboManager); sendKeyEvent(KeyEvent.ACTION_UP, modifier, keyCode, keyComboManager); } /** * Sends key event. This method first tries to send key event to KeyComboManager to simulate the * situation where AccessibilityService can listen and consume key events before an activity * gets them. */ private void sendKeyEvent(int action, int modifier, int keyCode, KeyComboManager keyComboManager) { KeyEvent keyEvent = new KeyEvent(KEY_EVENT_DOWN_TIME, KEY_EVENT_EVENT_TIME, action, keyCode, 0, modifier); SendKeyEventToKeyComboManagerRunnable runnable = new SendKeyEventToKeyComboManagerRunnable(keyComboManager, keyEvent); getInstrumentation().runOnMainSync(runnable); getInstrumentation().waitForIdleSync(); if (runnable.consumed) { return; } getInstrumentation().sendKeySync(keyEvent); getInstrumentation().waitForIdleSync(); } private static class SendKeyEventToKeyComboManagerRunnable implements Runnable { private final KeyComboManager mKeyComboManager; private final KeyEvent mKeyEvent; public boolean consumed; public SendKeyEventToKeyComboManagerRunnable(KeyComboManager keyComboManager, KeyEvent keyEvent) { mKeyComboManager = keyComboManager; mKeyEvent = keyEvent; } @Override public void run() { consumed = mKeyComboManager.onKeyEvent(mKeyEvent); } } /** * Gets layout direction of dialog preference. */ protected int getLayoutDirection(View view) { GetLayoutDirectionRunnable runnable = new GetLayoutDirectionRunnable(view); getInstrumentation().runOnMainSync(runnable); getInstrumentation().waitForIdleSync(); return runnable.mLayoutDirection; } private static class GetLayoutDirectionRunnable implements Runnable { private final View mView; public int mLayoutDirection; public GetLayoutDirectionRunnable(View view) { mView = view; } @Override public void run() { mLayoutDirection = mView.findViewById(android.R.id.content).getLayoutDirection(); } } }