/* * Copyright 2011-2015, Institute of Cybernetics at Tallinn University of Technology * * 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 ee.ioc.phon.android.speak.utils; import android.annotation.TargetApi; import android.app.Activity; import android.app.AlertDialog; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutManager; import android.content.res.Resources; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.speech.RecognizerIntent; import android.text.SpannableString; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.inputmethod.EditorInfo; import android.widget.EditText; import org.apache.commons.io.FileUtils; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.ShortBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Set; import ee.ioc.phon.android.speak.Executable; import ee.ioc.phon.android.speak.ExecutableString; import ee.ioc.phon.android.speak.Log; import ee.ioc.phon.android.speak.R; import ee.ioc.phon.android.speak.activity.SpeechActionActivity; import ee.ioc.phon.android.speak.model.CallerInfo; import ee.ioc.phon.android.speak.model.Combo; import ee.ioc.phon.android.speechutils.Extras; import ee.ioc.phon.android.speechutils.editor.CommandMatcher; import ee.ioc.phon.android.speechutils.editor.CommandMatcherFactory; import ee.ioc.phon.android.speechutils.editor.UtteranceRewriter; import ee.ioc.phon.android.speechutils.utils.PreferenceUtils; /** * <p>Some useful static methods.</p> * * @author Kaarel Kaljurand */ public final class Utils { private Utils() { } /** * TODO: should we immediately return null if id = 0? */ public static String idToValue(Context context, Uri contentUri, String columnId, String columnUrl, long id) { String value = null; Cursor c = context.getContentResolver().query( contentUri, new String[]{columnUrl}, columnId + "= ?", new String[]{String.valueOf(id)}, null); if (c.moveToFirst()) { value = c.getString(0); } c.close(); return value; } /** * <p>Pretty-prints an integer value which expresses a size * of some data.</p> */ public static String getSizeAsString(int size) { if (size > FileUtils.ONE_MB) { return String.format("%.1fMB", (float) size / FileUtils.ONE_MB); } if (size > FileUtils.ONE_KB) { return String.format("%.1fkB", (float) size / FileUtils.ONE_KB); } return size + "b"; } /** * <p>Returns a bitmap that visualizes the given waveform (byte array), * i.e. a sequence of 16-bit integers.</p> * <p/> * TODO: show to high/low points in other color * TODO: show end pause data with another color */ public static Bitmap drawWaveform(byte[] waveBuffer, int w, int h, int start, int end) { final Bitmap b = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); final Canvas c = new Canvas(b); final Paint paint = new Paint(); paint.setColor(0xFFFFFFFF); // 0xRRGGBBAA paint.setAntiAlias(true); paint.setStrokeWidth(0); final Paint redPaint = new Paint(); redPaint.setColor(0xFF000080); redPaint.setAntiAlias(true); redPaint.setStrokeWidth(0); final ShortBuffer buf = ByteBuffer.wrap(waveBuffer).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer(); buf.position(0); final int numSamples = waveBuffer.length / 2; //final int delay = (SAMPLING_RATE * 100 / 1000); final int delay = 0; int endIndex = end / 2 + delay; if (end == 0 || endIndex >= numSamples) { endIndex = numSamples; } int index = start / 2 - delay; if (index < 0) { index = 0; } final int size = endIndex - index; int numSamplePerPixel = 32; int delta = size / (numSamplePerPixel * w); if (delta == 0) { numSamplePerPixel = size / w; delta = 1; } final float scale = 3.5f / 65536.0f; // do one less column to make sure we won't read past // the buffer. try { for (int i = 0; i < w - 1; i++) { final float x = i; for (int j = 0; j < numSamplePerPixel; j++) { final short s = buf.get(index); final float y = (h / 2) - (s * h * scale); if (s > Short.MAX_VALUE - 10 || s < Short.MIN_VALUE + 10) { // TODO: make it work c.drawPoint(x, y, redPaint); } else { c.drawPoint(x, y, paint); } index += delta; } } } catch (IndexOutOfBoundsException e) { // this can happen, but we don't care } return b; } /** * Creates a non-cancelable dialog with two buttons, both finish the activity, * one launches the given intent first. * TODO: note that we explicitly set the dialog style. This is because if the caller activity's style * is Theme.Translucent.NoTitleBar then the dialog is unstyled (maybe an Android bug?) */ public static AlertDialog getLaunchIntentDialog(final Activity activity, String msg, final Intent intent) { return new AlertDialog.Builder(activity, android.R.style.Theme_DeviceDefault_Dialog) .setPositiveButton(activity.getString(R.string.buttonGoToSettings), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { activity.startActivity(intent); activity.finish(); } }) .setNegativeButton(activity.getString(R.string.buttonCancel), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.cancel(); activity.finish(); } }) .setMessage(msg) .setCancelable(false) .create(); } public static AlertDialog getYesNoDialog(Context context, String confirmationMessage, final Executable ex) { AlertDialog.Builder builder = new AlertDialog.Builder(context); builder .setMessage(confirmationMessage) .setCancelable(false) .setPositiveButton(context.getString(R.string.buttonYes), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { ex.execute(); } }) .setNegativeButton(context.getString(R.string.buttonNo), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.cancel(); } }); return builder.create(); } public static AlertDialog getTextEntryDialog(Context context, String title, String initialText, final ExecutableString ex) { final View textEntryView = LayoutInflater.from(context).inflate(R.layout.alert_dialog_url_entry, null); final EditText et = (EditText) textEntryView.findViewById(R.id.url_edit); if (initialText != null) { et.setText(initialText); et.setSelection(initialText.length()); } return new AlertDialog.Builder(context) .setTitle(title) .setView(textEntryView) .setPositiveButton(R.string.buttonOk, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { ex.execute(et.getText().toString()); } }) .setNegativeButton(R.string.buttonCancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { dialog.cancel(); } }) .create(); } public static String getVersionName(Context c) { PackageInfo info = getPackageInfo(c); if (info == null) { return "?.?.?"; } return info.versionName; } private static PackageInfo getPackageInfo(Context c) { PackageManager manager = c.getPackageManager(); try { return manager.getPackageInfo(c.getPackageName(), 0); } catch (NameNotFoundException e) { Log.e("Couldn't find package information in PackageManager: " + e); } return null; } public static String chooseValue(String firstChoice, String secondChoice) { if (firstChoice == null) { return secondChoice; } return firstChoice; } public static String chooseValue(String firstChoice, String secondChoice, String thirdChoice) { String choice = chooseValue(firstChoice, secondChoice); if (choice == null) { return thirdChoice; } return choice; } public static List<String> ppBundle(Bundle bundle) { return ppBundle("/", bundle); } private static List<String> ppBundle(String bundleName, Bundle bundle) { List<String> strings = new ArrayList<>(); if (bundle == null) { return strings; } for (String key : bundle.keySet()) { Object value = bundle.get(key); String name = bundleName + key; if (value instanceof Bundle) { strings.addAll(ppBundle(name + "/", (Bundle) value)); } else { if (value instanceof Object[]) { strings.add(name + ": " + Arrays.toString((Object[]) value)); } else if (value instanceof float[]) { strings.add(name + ": " + Arrays.toString((float[]) value)); } else { strings.add(name + ": " + value); } } } return strings; } /** * <p>Traverses the given bundle looking for the given key. The search also * looks into embedded bundles and thus differs from {@code Bundle.get(String)}. * Returns the first found entry as an object. If the given bundle does not * contain the given key then returns {@code null}.</p> * * @param bundle bundle (e.g. intent extras) * @param key key of a bundle entry (possibly in an embedded bundle) * @return first matching key's value */ public static Object getBundleValue(Bundle bundle, String key) { for (String k : bundle.keySet()) { Object value = bundle.get(k); if (value instanceof Bundle) { Object deepValue = getBundleValue((Bundle) value, key); if (deepValue != null) { return deepValue; } } else if (key.equals(k)) { return value; } } return null; } public static String makeUserAgentComment(String tag, String versionName, String caller) { return tag + "/" + versionName + "; " + Build.MANUFACTURER + "/" + Build.DEVICE + "/" + Build.DISPLAY + "; " + caller; } /** * Generates rewriters based on the list of names of rewrite tables. * If a name does not resolve to a rewrite table then generates null. * If the given list is null, then the default rewriter is returned (currently at most one). * Passing an empty list effectively turns off rewriting. */ public static Iterable<UtteranceRewriter> genRewriters(final SharedPreferences prefs, final Resources resources, String[] rewritesByName, String language, ComponentName service, ComponentName app) { final String[] names; if (rewritesByName == null) { Set<String> defaults = PreferenceUtils.getPrefStringSet(prefs, resources, R.string.defaultRewriteTables); if (defaults.isEmpty()) { return Collections.EMPTY_LIST; } names = defaults.toArray(new String[defaults.size()]); // TODO: defaults should be a list (not a set that needs to be sorted) Arrays.sort(names); } else { names = rewritesByName; } final int length = names.length; if (length == 0) { return Collections.EMPTY_LIST; } final CommandMatcher commandMatcher = CommandMatcherFactory.createCommandFilter(language, service, app); return new Iterable<UtteranceRewriter>() { @Override public Iterator<UtteranceRewriter> iterator() { return new Iterator<UtteranceRewriter>() { private int mCurrent = 0; @Override public boolean hasNext() { return mCurrent < length; } @Override public UtteranceRewriter next() { String rewritesAsStr = PreferenceUtils.getPrefMapEntry(prefs, resources, R.string.keyRewritesMap, names[mCurrent++]); if (rewritesAsStr == null) { return null; } return new UtteranceRewriter(rewritesAsStr, commandMatcher); } }; } }; } public static <E> List<E> makeList(Iterable<E> iter) { List<E> list = new ArrayList<>(); for (E item : iter) { list.add(item); } return list; } public static Intent getRecognizerIntent(String action, CallerInfo callerInfo, String language) { Intent intent = new Intent(action); Bundle extras = callerInfo.getExtras(); if (extras != null) { intent.putExtras(extras); } intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM); intent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true); intent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, callerInfo.getPackageName()); if (callerInfo.getEditorInfo() != null) { intent.putExtra(Extras.EXTRA_EDITOR_INFO, toBundle(callerInfo.getEditorInfo())); } // Declaring that in the IME we would like to allow longer pauses (2 sec). // The service might not implement these (e.g. Kõnele currently does not) // TODO: what is the difference of these two constants? //intent.putExtra(RecognizerIntent.EXTRA_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS, 2000); //intent.putExtra(RecognizerIntent.EXTRA_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS, 2000); if (language != null) { intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language); // TODO: make this configurable intent.putExtra(Extras.EXTRA_ADDITIONAL_LANGUAGES, new String[]{}); } return intent; } /** * Constructs and publishes the list of app shortcuts, one for each combo that is selected for the * search panel. The intent behind the shortcut sets AUTO_START=true and sets RESULTS_REWRITES * to the list of default rewrites (at creation time), and PROMPT to the list of rewrite names. * All other settings (e.g. MAX_RESULTS) depend on the settings at execution time. */ @TargetApi(Build.VERSION_CODES.N_MR1) public static void publishShortcuts(Context context, List<Combo> selectedCombos, Set<String> rewriteTables) { ShortcutManager shortcutManager = context.getSystemService(ShortcutManager.class); List<ShortcutInfo> shortcuts = new ArrayList<>(); int maxShortcutCountPerActivity = shortcutManager.getMaxShortcutCountPerActivity(); int counter = 0; // TODO: rewriteTables should be a list (not a set that needs to be sorted) String[] names = rewriteTables.toArray(new String[rewriteTables.size()]); Arrays.sort(names); String rewritesId = TextUtils.join(", ", names); for (Combo combo : selectedCombos) { Intent intent = new Intent(context, SpeechActionActivity.class); intent.setAction(RecognizerIntent.ACTION_WEB_SEARCH); intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, combo.getLocaleAsStr()); intent.putExtra(Extras.EXTRA_SERVICE_COMPONENT, combo.getServiceComponent().flattenToShortString()); if (names.length > 0) { intent.putExtra(RecognizerIntent.EXTRA_PROMPT, rewritesId); } intent.putExtra(Extras.EXTRA_RESULT_REWRITES, names); intent.putExtra(Extras.EXTRA_AUTO_START, true); // Launch the activity so that the existing Kõnele activities are not in the background stack. intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); shortcuts.add(new ShortcutInfo.Builder(context, combo.getId() + rewritesId) .setIntent(intent) .setShortLabel(combo.getShortLabel()) .setLongLabel(combo.getLongLabel() + "; " + rewritesId) .setIcon(Icon.createWithBitmap(drawableToBitmap(combo.getIcon(context)))) .build()); counter++; // We are only allowed a certain number (5) of shortcuts if (counter >= maxShortcutCountPerActivity) { break; } } shortcutManager.setDynamicShortcuts(shortcuts); } private static Bundle toBundle(EditorInfo attribute) { Bundle bundle = new Bundle(); bundle.putBundle("extras", attribute.extras); bundle.putInt("inputType", attribute.inputType); bundle.putInt("initialSelStart", attribute.initialSelStart); bundle.putInt("initialSelEnd", attribute.initialSelEnd); bundle.putString("actionLabel", asString(attribute.actionLabel)); bundle.putString("fieldName", asString(attribute.fieldName)); bundle.putString("hintText", asString(attribute.hintText)); bundle.putString("label", asString(attribute.label)); // This line gets the actual caller package registered in the package registry. // The key needs to be "packageName". bundle.putString("packageName", asString(attribute.packageName)); return bundle; } private static String asString(Object o) { if (o == null) { return null; } if (o instanceof SpannableString) { SpannableString ss = (SpannableString) o; return ss.subSequence(0, ss.length()).toString(); } return o.toString(); } /** * This is needed to convert a Drawable to an Icon (we need to convert the Drawable first * to Bitmap). Solution from * http://stackoverflow.com/questions/3035692/how-to-convert-a-drawable-to-a-bitmap * Starting with API 23, we might make combo.getIcon return Icon (instead of a Drawable), which * will simplify things. */ private static Bitmap drawableToBitmap(Drawable drawable) { Bitmap bitmap = null; if (drawable instanceof BitmapDrawable) { BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable; if (bitmapDrawable.getBitmap() != null) { return bitmapDrawable.getBitmap(); } } if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) { bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); // Single color bitmap will be created of 1x1 pixel } else { bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); } Canvas canvas = new Canvas(bitmap); drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); drawable.draw(canvas); return bitmap; } }