I am trying to create a password app manager. I want to create an Autofill feature.
I saw Android autofill, but it doesnt work perfectly on browser app. So I decided to use the accessiblity to get all editText.
If I find 2 EditText including one in password mode, I enable autofill. (I suppose it is a login form)
When I loop into all nodes in the view, I will only find the EditText focused.it's like this I couldn't find all the node.
Is there a way to find all the node of the screen for a browser app?
Here is the code
#Override
public void onAccessibilityEvent(AccessibilityEvent event) {
switch(event.getEventType()) {
case AccessibilityEvent.TYPE_VIEW_CLICKED:
case AccessibilityEvent.TYPE_VIEW_FOCUSED:
try {
if(event.getClassName() == null) return;
Log.e(TAG, "onAccessibilityEvent: " + getEventType(event) );
Class className = Class.forName(event.getClassName().toString());
if (EditText.class.isAssignableFrom(className)) {
/** parse ALL NODE -> see getCredFieldsFromViews **/
Log.i(TAG + "-info", AccessiblityParser.getInfoFromEvent(getRootInActiveWindow()).toString());
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
break;
}
}
#Override
public void onServiceConnected() {
AccessibilityServiceInfo info = getServiceInfo();
info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;
info.feedbackType = AccessibilityServiceInfo.FEEDBACK_ALL_MASK; //.CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT;
info.flags = AccessibilityServiceInfo.DEFAULT | AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS |
AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE | AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS
| AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS;
//info.flags = AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS ;
info.notificationTimeout = 100;
this.setServiceInfo(info);
}
Loop all the nodes :
private static void getCredFieldsFromViews(AccessiblityParser accessiblityParser,AccessibilityNodeInfo info) throws ClassNotFoundException {
if (info == null)
return;
if(info.isEditable()){
if( info.isPassword() ){ // password EDIT TEXT
accessiblityParser.mdpNode = info;
}else{ // (maybe) username EDIT TEXT
accessiblityParser.usernameNode = info;
}
}
int childCount = info.getChildCount();
for(int i=0;i<childCount;i++){
AccessibilityNodeInfo child = info.getChild(i);
if(child != null)
child.refresh();
getCredFieldsFromViews(accessiblityParser,child);
if(child != null){
child.recycle();
}
test++;
}
return;
}
EDIT :
I saw Accessibility and Android WebView
Maybe I can inject script if it is not deprecated ?
Another way maybe use :
arguments.putString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING, "INPUT");
r = info.performAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, arguments);
But how to use it ?
Thank
Related
I am planning to create an Android App that will automate sending a users response to a USSD Menu. At a click of a button, the app will send the initial code, followed by the rest of the menu inputs.
For example, the initial number is *143#, followed by 1, 1, 1, and a user PIN. I'd like to be able to automate that sequence of inputs so that the user won't have to input it on their own.
I know that in Android Oreo, they implemented a USSD Callback using the TelephonyManager, where the Android App can send a USSD Request, and then read the response given.
I am currently exploring that option and this is what I've tried so far. Heavily lifted from this StackOverflow Question.
interface UssdResultNotifiable {
void notifyUssdResult(String request, String returnMessage, int resultCode);
}
public class MainActivity extends Activity implements UssdResultNotifiable {
USSDSessionHandler hdl;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void onUssdSend(View view) {
hdl = new USSDSessionHandler(MainActivity.this, MainActivity.this);
hdl.doSession(((EditText) this.findViewById(R.id.ussdText)).getText().toString());
}
#Override
public void notifyUssdResult(final String request, final String returnMessage, final int resultCode) {
this.runOnUiThread(new Runnable() {
#Override
public void run() {
Toast.makeText(MainActivity.this, "Request was " + request + "\n response is "
+ returnMessage + "\n result code is " + resultCode, Toast.LENGTH_LONG).show();
Log.e(TAG, "ussd result hit! Response is = " + returnMessage);
hdl.doSession("1");
}
});
}
}
class USSDSessionHandler {
TelephonyManager tm;
private UssdResultNotifiable client;
private Method handleUssdRequest;
private Object iTelephony;
USSDSessionHandler(Context parent, UssdResultNotifiable client) {
this.client = client;
this.tm = (TelephonyManager) parent.getSystemService(Context.TELEPHONY_SERVICE);
try {
this.getUssdRequestMethod();
} catch (Exception ex) {
//log
}
}
private void getUssdRequestMethod() throws ClassNotFoundException, NoSuchMethodException,
InvocationTargetException, IllegalAccessException {
if (tm != null) {
Class telephonyManagerClass = Class.forName(tm.getClass().getName());
if (telephonyManagerClass != null) {
Method getITelephony = telephonyManagerClass.getDeclaredMethod("getITelephony");
getITelephony.setAccessible(true);
this.iTelephony = getITelephony.invoke(tm); // Get the internal ITelephony object
Method[] methodList = iTelephony.getClass().getMethods();
this.handleUssdRequest = null;
for (Method _m : methodList)
if (_m.getName().equals("handleUssdRequest")) {
handleUssdRequest = _m;
break;
}
}
}
}
#android.support.annotation.RequiresApi(api = Build.VERSION_CODES.N)
public void doSession(String ussdRequest) {
try {
if (handleUssdRequest != null) {
handleUssdRequest.setAccessible(true);
handleUssdRequest.invoke(iTelephony, SubscriptionManager.getDefaultSubscriptionId(), ussdRequest, new ResultReceiver(new Handler()) {
#Override
protected void onReceiveResult(int resultCode, Bundle ussdResponse) {
Object p = ussdResponse.getParcelable("USSD_RESPONSE");
if (p != null) {
Method[] methodList = p.getClass().getMethods();
for(Method m : methodList){
Log.e(TAG, "onReceiveResult: " + m.getName());
}
try {
CharSequence returnMessage = (CharSequence) p.getClass().getMethod("getReturnMessage").invoke(p);
CharSequence request = (CharSequence) p.getClass().getMethod("getUssdRequest").invoke(p);
USSDSessionHandler.this.client.notifyUssdResult("" + request, "" + returnMessage, resultCode); //they could be null
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
});
}
} catch (IllegalAccessException | InvocationTargetException e1) {
e1.printStackTrace();
}
}
}
What you get is an app that would ask for a USSD Input for the user, and once the "Send" button is hit, the USSD Response is displayed via a Toast on the notifyUssdResult function. In that same function, I send the next input in my sequence which is "1". Am I able to once again send a reply to the USSD, and the USSD takes it as the input and goes to the next menu.
However, as soon as I send the reply, the USSD menu shows up in my device and I'm unable to proceed further. I am unable to navigate the USSD Menu purely through my app, as the USSD Menu interferes with the screen and doesn't go away.
Is there any samples I can follow?
FOR EXAMPLE YOU WANT TO RUN *123# 1 then 1 then 1 then 2
so you can directly write this in string here is the code hope it helps
String s= "*123*1*1*2#"
//screen from which can get the number
if((s.startsWith("*"))&&(s.endsWith("#"))){
//if true then it is a USSD call----
callstring=s.substring(0, s.length()-1);
callstring=callstring+ Uri.encode("#");
Log.d("CALL TYPE---------->", "USSD CALL");
}else{
callstring=s;
Log.d("CALL TYPE---------->", "Not a USSD CALL");
}
Intent i=new Intent(Intent.ACTION_CALL,Uri.parse("tel:"+callstring));
startActivity(i);
I want to find the URL of the current tab using accessibility service. When I open the tab I can't get the URL, but I can get the texts displayed on the screen.
I am using the following code:`
public class ASUrl extends AccessibilityService {
private static final String TAG = ASUrl.class
.getSimpleName();
#Override
public void onAccessibilityEvent(AccessibilityEvent event) {
AccessibilityNodeInfo source = event.getSource();
if (source == null)
return;
final String packageName = String.valueOf(source.getPackageName());
// Add browser package list here (comma seperated values)
String BROWSER_LIST = "";
List<String> browserList
= Arrays.asList(BROWSER_LIST.split(",\\s*"));
if (event.getEventType()
== AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
if (!browserList.contains(packageName)) {
return;
}
}
if (browserList.contains(packageName)) {
try {
// App opened is a browser.
// Parse urls in browser.
if (AccessibilityEvent
.eventTypeToString(event.getEventType())
.contains("WINDOW")) {
AccessibilityNodeInfo nodeInfo = event.getSource();
getUrlsFromViews(nodeInfo);
}
} catch(StackOverflowError ex){
ex.printStackTrace();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
/**
* Method to loop through all the views and try to find a URL.
* #param info
*/
public void getUrlsFromViews(AccessibilityNodeInfo info) {
try {
if (info == null)
return;
if (info.getText() != null && info.getText().length() > 0) {
String capturedText = info.getText().toString();
if (capturedText.contains("https://")
|| capturedText.contains("http://")) {
Log.i(TAG, "Captured "+ capturedText);
}
}
for (int i = 0; i < info.getChildCount(); i++) {
AccessibilityNodeInfo child = info.getChild(i);
getUrlsFromViews(child);
if(child != null){
child.recycle();
}
}
} catch(StackOverflowError ex){
ex.printStackTrace();
} catch (Exception ex) {
ex.printStackTrace();
}
}
#Override
public void onInterrupt() {
}
}`
I can't get the url. My XML is below:
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagDefault|flagIncludeNotImportantViews|flagRequestTouchExplorationMode|flagRequestEnhancedWebAccessibility|flagReportViewIds|flagRetrieveInteractiveWindows"
android:canPerformGestures="true"
android:canRetrieveWindowContent="true"
android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged|typeAllMask"
/>
I have looked at other solutions like Android : Read Google chrome URL using accessibility service, but this didn't help.
The code gives the URL the first time, but it doesn't work after that.
I am testing on Android 8.1.0, API27.
After opening application details settings using
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS), how can I force stop application programmatically?
You can use Accessibility to achieve that (but it needs Accessibility for your app turned on by user)
public class MyAccessibilityService extends AccessibilityService {
#Override
public void onAccessibilityEvent(AccessibilityEvent event) {
//TYPE_WINDOW_STATE_CHANGED == 32
if (AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED == event
.getEventType()) {
AccessibilityNodeInfo nodeInfo = event.getSource();
if (nodeInfo == null) {
return;
}
List<AccessibilityNodeInfo> list = nodeInfo
.findAccessibilityNodeInfosByViewId("com.android.settings:id/left_button");
//We can find button using button name or button id
for (AccessibilityNodeInfo node : list) {
node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
}
list = nodeInfo
.findAccessibilityNodeInfosByViewId("android:id/button1");
for (AccessibilityNodeInfo node : list) {
node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
}
}
}
#Override
public void onInterrupt() {
// TODO Auto-generated method stub
}
}
You can check it out in this example:
AccessibilityTestService.java
You have two ways, a more rude one and a better one
The good practice
If you have only one activity running
the this.finish(); method will be enough
If you have multiple activities running
You gotta call the this.finishAffinity(); method. This is the best practice in general cases, where you can have both a single or multiple activities
The rude way
System.Exit(0);
I added this only for info, but this might not work with multiple activities and this is not a good way for closing apps. It's mostly like the "Hold power button until the pc shuts down".
Clicking an element of another application on runtime is something that will be considered as a security threat. You would need a hack to go past this hurdle.
There is one hack that I recently found out, you can probably make use of it. You can find the source code here: https://github.com/tfKamran/android-ui-automator
You can add the code in here as a module in your app and invoke a service with action com.tf.uiautomator.ACTION_CLICK_ITEM and send the text of the element you want to click on as an extra with key itemText.
You can test it using adb like:
adb shell am startservice -a com.tf.uiautomator.ACTION_CLICK_ITEM -e itemText "OK"
I found one solution for force stop. After force stop how can i go back to my activity page ?
public class DeviceAccessibilityService extends AccessibilityService {
private static final String TAG = "litan";
private boolean isKilled = false;
#Override
public int onStartCommand(Intent intent, int flags, int startId) {
isKilled = false;
return super.onStartCommand(intent, flags, startId);
}
#Override
public void onAccessibilityEvent(AccessibilityEvent event) {
if (AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED == event.getEventType()) {
AccessibilityNodeInfo nodeInfo = event.getSource();
Log.i(TAG, "ACC::onAccessibilityEvent: nodeInfo=" + nodeInfo);
if (nodeInfo == null) {
return;
}
List<AccessibilityNodeInfo> list = new ArrayList<>();
if ("com.android.settings.applications.InstalledAppDetailsTop".equals(event.getClassName())) {
if (Build.VERSION.SDK_INT >= 18) {
list = nodeInfo.findAccessibilityNodeInfosByViewId("com.android.settings:id/right_button");
} else if (Build.VERSION.SDK_INT >= 14) {
list = nodeInfo.findAccessibilityNodeInfosByText("com.android.settings:id/right_button");
}
for (AccessibilityNodeInfo node : list) {
Log.i(TAG, "ACC::onAccessibilityEvent: left_button " + node);
node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
}
} else if ("android.app.AlertDialog".equals(event.getClassName())) {
list = new ArrayList<>();
if (Build.VERSION.SDK_INT >= 18) {
list = nodeInfo.findAccessibilityNodeInfosByViewId("android:id/button1");
} else if (Build.VERSION.SDK_INT >= 14) {
list = nodeInfo.findAccessibilityNodeInfosByText("android:id/button1");
}
for (final AccessibilityNodeInfo node : list) {
Log.i(TAG, "ACC::onAccessibilityEvent: button1 " + node);
node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
//node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
}
}
return;
}
}
#Override
public void onInterrupt() {
// TODO Auto-generated method stub
Log.i("Interrupt", "Interrupt");
}
#Override
protected void onServiceConnected() {
AccessibilityServiceInfo info = getServiceInfo();
info.eventTypes = AccessibilityEvent.TYPE_WINDOWS_CHANGED | AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED | AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
info.flags = AccessibilityServiceInfo.DEFAULT;
info.flags = AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
info.flags = AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS;
info.flags = AccessibilityServiceInfo.FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY;
info.flags = AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS;
// We are keeping the timeout to 0 as we don’t need any delay or to pause our accessibility events
info.feedbackType = AccessibilityEvent.TYPES_ALL_MASK;
info.notificationTimeout = 100;
this.setServiceInfo(info);
// Toast.makeText(getApplicationContext(), "onServiceConnected", Toast.LENGTH_SHORT).show();
}
private static void logd(String msg) {
Log.d(TAG, msg);
}
private static void logw(String msg) {
Log.w(TAG, msg);
}
private static void logi(String msg) {
Log.i(TAG, msg);
}
}
I am using CSipSimple for adding a new feature which is transfer call . For this feature I need the callID of call .
I am seeing that when I call ,the following function is called .
public void placeCallWithOption(Bundle b) {
if (service == null) {
return;
}
String toCall = "";
Long accountToUse = SipProfile.INVALID_ID;
// Find account to use
SipProfile acc = accountChooserButton.getSelectedAccount();
if(acc == null) {
return;
}
accountToUse = acc.id;
// Find number to dial
toCall = digits.getText().toString();
if(isDigit) {
toCall = PhoneNumberUtils.stripSeparators(toCall);
}
if(accountChooserFilterItem != null && accountChooserFilterItem.isChecked()) {
toCall = rewriteNumber(toCall);
}
if (TextUtils.isEmpty(toCall)) {
return;
}
// Well we have now the fields, clear theses fields
digits.getText().clear();
// -- MAKE THE CALL --//
if (accountToUse >= 0) {
// It is a SIP account, try to call service for that
try {
service.makeCallWithOptions(toCall, accountToUse.intValue(), b);
// service.xfer(callId,"01628105601");
} catch (RemoteException e) {
Log.e(THIS_FILE, "Service can't be called to make the call");
}
} else if (accountToUse != SipProfile.INVALID_ID) {
// It's an external account, find correct external account
CallHandlerPlugin ch = new CallHandlerPlugin(getActivity());
ch.loadFrom(accountToUse, toCall, new OnLoadListener() {
#Override
public void onLoad(CallHandlerPlugin ch) {
placePluginCall(ch);
}
});
}
}
But from this , I can't get callId of the call. How can I get callId of each call ? Any advice is of great help .
Using SipCallSession to fetch a call id
SipCallSession callInfo = new SipCallSession();
callinfo.getCallid();
I'm playing text with android TTS - android.speech.tts.TextToSpeech
I use: TextToSpeech.speak to speak and .stop to stop. Is there a way to pause the text also?
The TTS SDK doesn't have any pause functionality that I know of. But you could use synthesizeToFile() to create an audio file that contains the TTS output. Then, you would use a MediaPlayer object to play, pause, and stop playing the file. Depending on how long the text string is, it might take a little longer for audio to be produced because the synthesizeToFile() function would have to complete the entire file before you could play it, but this delay should be acceptable for most applications.
I used splitting of string and used playsilence() like below:
public void speakSpeech(String speech) {
HashMap<String, String> myHash = new HashMap<String, String>();
myHash.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "done");
String[] splitspeech = speech.split("\\.");
for (int i = 0; i < splitspeech.length; i++) {
if (i == 0) { // Use for the first splited text to flush on audio stream
textToSpeech.speak(splitspeech[i].toString().trim(),TextToSpeech.QUEUE_FLUSH, myHash);
} else { // add the new test on previous then play the TTS
textToSpeech.speak(splitspeech[i].toString().trim(), TextToSpeech.QUEUE_ADD,myHash);
}
textToSpeech.playSilence(750, TextToSpeech.QUEUE_ADD, null);
}
}
You can make the TTS pause between sentences, or anywhere you want by adding up to three periods (".") all followed by a single space " ". The example below has a long pause at the beginning, and again before the message body. I'm not sure that is what you are after though.
private final BroadcastReceiver SMScatcher = new BroadcastReceiver() {
#Override
public void onReceive(final Context context, final Intent intent) {
if (intent.getAction().equals(
"android.provider.Telephony.SMS_RECEIVED")) {
// if(message starts with SMStretcher recognize BYTE)
StringBuilder sb = new StringBuilder();
/*
* The SMS-Messages are 'hiding' within the extras of the
* Intent.
*/
Bundle bundle = intent.getExtras();
if (bundle != null) {
/* Get all messages contained in the Intent */
Object[] pdusObj = (Object[]) bundle.get("pdus");
SmsMessage[] messages = new SmsMessage[pdusObj.length];
for (int i = 0; i < pdusObj.length; i++) {
messages[i] = SmsMessage
.createFromPdu((byte[]) pdusObj[i]);
}
/* Feed the StringBuilder with all Messages found. */
for (SmsMessage currentMessage : messages) {
// periods are to pause
sb.append("... Message From: ");
/* Sender-Number */
sb.append(currentMessage.getDisplayOriginatingAddress());
sb.append(".. ");
/* Actual Message-Content */
sb.append(currentMessage.getDisplayMessageBody());
}
// Toast.makeText(application, sb.toString(),
// Toast.LENGTH_LONG).show();
if (mTtsReady) {
try {
mTts.speak(sb.toString(), TextToSpeech.QUEUE_ADD,
null);
} catch (Exception e) {
Toast.makeText(application, "TTS Not ready",
Toast.LENGTH_LONG).show();
e.printStackTrace();
}
}
}
}
}
};
If you omit the space after the last period it will (or may) not work as expected.
In the absence of a pause option, you can add silence for the duration of when you want to delay the TTS Engine speaking. This of course would have to be a predetermined 'pause' and wouldn't help to include functionality of a pause button, for example.
For API < 21 : public int playSilence (long durationInMs, int queueMode, HashMap params)
For > 21 : public int playSilentUtterance (long durationInMs, int queueMode, String utteranceId)
Remember to use TextToSpeech.QUEUE_ADD rather than TextToSpeech.QUEUE_FLUSH otherwise it will clear the previously started speech.
I used a different approach.
Seperate your text into sentences
Speak every sentence one by one and keep track of the spoken sentence
pause will stop the text instantly
resume will start at the beginning of the last spoken sentence
Kotlin code:
class VoiceService {
private lateinit var textToSpeech: TextToSpeech
var sentenceCounter: Int = 0
var myList: List<String> = ArrayList()
fun resume() {
sentenceCounter -= 1
speakText()
}
fun pause() {
textToSpeech.stop()
}
fun stop() {
sentenceCounter = 0
textToSpeech.stop()
}
fun speakText() {
var myText = "This is some text to speak. This is more text to speak."
myList =myText.split(".")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
textToSpeech.speak(myList[sentenceCounter], TextToSpeech.QUEUE_FLUSH, null, utteranceId)
sentenceCounter++
} else {
var map: HashMap<String, String> = LinkedHashMap<String, String>()
map[TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID] = utteranceId
textToSpeech.speak(myList[sentenceCounter], TextToSpeech.QUEUE_FLUSH, map)
sentenceCounter++
}
}
override fun onDone(p0: String?) {
if (sentenceCounter < myList.size) {
speakText()
} else {
speakNextText()
}
}
}
I haven't yet tried this, but I need to do the same thing. My thinking is to first split your speech text into an array of words.
Then create a recursive function that plays the next word after the current word is finished, while keeping a counter of the current word.
divide the messages into parts and listen for last utterance by using onutteranceprogress listener
tts.playSilence(1250, TextToSpeech.QUEUE_ADD, null);
It seems that if you put a period after a word AND start the next word with a capital letter, just like a new sentence, like this:
after we came home. We ate dinner.
the "home. We" will then have a pause in it.
This becomes a grammatically strange way of writing it.
So far I have only tested this in my own language, Swedish.
It might be important that the space is there.
Also, an escaped quote (\") seems to have it pause somewhat as well - at least, if you put it around a word it adds space around the word.
This solution is not perfect, but an alternative to #Aaron C's solution may be to create a custom text to speech class like the below. This solution may work well enough if your text is relatively short and spoken words per minute is accurate enough for the language you are using.
private class CustomTextToSpeech extends TextToSpeech {
private static final double WORDS_PER_MS = (double)190/60/1000;
long startTimestamp = 0;
long pauseTimestamp = 0;
private Handler handler;
private Runnable speakRunnable;
StringBuilder textToSpeechBuilder;
private boolean isPaused = false;
public CustomTextToSpeech(Context context, OnInitListener initListener){
super(context, initListener);
setOnUtteranceProgressListener(new UtteranceProgressListener() {
#Override
public void onDone(String arg0) {
Log.d(TAG, "tts done. " + arg0);
startTimestamp = 0;
pauseTimestamp = 0;
handler.postDelayed(speakRunnable, TTS_INTERVAL_MS);
}
#Override
public void onError(String arg0) {
Log.e(TAG, "tts error. " + arg0);
}
#Override
public void onStart(String arg0) {
Log.d(TAG, "tts start. " + arg0);
setStartTimestamp(System.currentTimeMillis());
}
});
handler = new Handler();
speakRunnable = new Runnable() {
#Override
public void run() {
speak();
}
};
textToSpeechBuilder = new StringBuilder(getResources().getString(R.string.talkback_tips));
}
public void setStartTimestamp(long timestamp) {
startTimestamp = timestamp;
}
public void setPauseTimestamp(long timestamp) {
pauseTimestamp = timestamp;
}
public boolean isPaused(){
return (startTimestamp > 0 && pauseTimestamp > 0);
}
public void resume(){
if(handler != null && isPaused){
if(startTimestamp > 0 && pauseTimestamp > 0){
handler.postDelayed(speakRunnable, TTS_SETUP_TIME_MS);
} else {
handler.postDelayed(speakRunnable, TTS_INTERVAL_MS);
}
}
isPaused = false;
}
public void pause(){
isPaused = true;
if (handler != null) {
handler.removeCallbacks(speakRunnable);
handler.removeMessages(1);
}
if(isSpeaking()){
setPauseTimestamp(System.currentTimeMillis());
}
stop();
}
public void utter(){
if(handler != null){
handler.postDelayed(speakRunnable, TTS_INTERVAL_MS);
}
}
public void speak(){
Log.d(TAG, "textToSpeechBuilder: " + textToSpeechBuilder.toString());
if(isPaused()){
String[] words = textToSpeechBuilder.toString().split(" ");
int wordsAlreadySpoken = (int)Math.round((pauseTimestamp - startTimestamp)*WORDS_PER_MS);
words = Arrays.copyOfRange(words, wordsAlreadySpoken-1, words.length);
textToSpeechBuilder = new StringBuilder();
for(String s : words){
textToSpeechBuilder.append(s);
textToSpeechBuilder.append(" ");
}
} else {
textToSpeechBuilder = new StringBuilder(getResources().getString(R.string.talkback_tips));
}
if (tts != null && languageAvailable)
speak(textToSpeechBuilder.toString(), TextToSpeech.QUEUE_FLUSH, new Bundle(), "utter");
}
}