Java tutorial
/* * Copyright (C) 2010-2012 Felix Bechstein, Lado Kumsiashvili * * This file is part of WebSMS. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 3 of the License, or (at your option) any later * version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * this program; If not, see <http://www.gnu.org/licenses/>. */ package de.ub0r.android.websms; import android.Manifest; import android.annotation.TargetApi; import android.app.Activity; import android.app.AlertDialog; import android.app.AlertDialog.Builder; import android.app.DatePickerDialog; import android.app.DatePickerDialog.OnDateSetListener; import android.app.Dialog; import android.app.TimePickerDialog.OnTimeSetListener; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.database.Cursor; import android.database.SQLException; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.support.v7.app.AppCompatActivity; import android.telephony.TelephonyManager; import android.text.ClipboardManager; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.text.format.DateFormat; import android.util.Base64; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; import android.view.ViewGroup; import android.view.Window; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.BaseAdapter; import android.widget.DatePicker; import android.widget.EditText; import android.widget.GridView; import android.widget.ImageView; import android.widget.MultiAutoCompleteTextView; import android.widget.TextView; import android.widget.TimePicker; import android.widget.Toast; import android.widget.ToggleButton; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.text.Normalizer; import java.util.ArrayList; import java.util.Calendar; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import de.ub0r.android.lib.DonationHelper; import de.ub0r.android.lib.apis.ContactsWrapper; import de.ub0r.android.websms.connector.common.Connector; import de.ub0r.android.websms.connector.common.ConnectorCommand; import de.ub0r.android.websms.connector.common.ConnectorSpec; import de.ub0r.android.websms.connector.common.ConnectorSpec.SubConnectorSpec; import de.ub0r.android.websms.connector.common.Log; import de.ub0r.android.websms.connector.common.SMSLengthCalculator; import de.ub0r.android.websms.connector.common.Utils; import de.ub0r.android.websms.connector.common.WebSMSException; import de.ub0r.android.websms.rules.PseudoConnectorRules; /** * Main Activity. * * @author flx */ public class WebSMS extends AppCompatActivity implements OnClickListener, OnDateSetListener, OnTimeSetListener, OnLongClickListener { /** * Tag for output. */ public static final String TAG = "main"; private static final String LAST_RUN = "last_run"; /** * Default SMS length calculator. */ private static final SMSLengthCalculator SMS_LENGTH_CALCULATOR = new DefaultSMSLengthCalculator(); public static final String DONATION_URL = "https://play.google.com/store/apps/details?id=de.ub0r.android.donator"; private static final int PERMISSIONS_REQUEST_READ_PHONE_STATE = 1; private static final int PERMISSIONS_REQUEST_READ_CONTACTS = 2; private static final int PERMISSIONS_REQUEST_SEND_SMS = 3; /** * Static reference to running Activity. */ private static WebSMS me; /** * Preference's name: user's phone number. */ static final String PREFS_SENDER = "sender"; /** * Preference's name: default prefix. */ static final String PREFS_DEFPREFIX = "defprefix"; /** * Preference's name: update balance on start. */ static final String PREFS_AUTOUPDATE = "autoupdate"; /** * Preference's name: exit after sending. */ private static final String PREFS_AUTOEXIT = "autoexit"; /** * Preference's name: show mobile numbers only. */ private static final String PREFS_MOBILES_ONLY = "mobiles_only"; /** * Preference's name: enable autosend. */ private static final String PREFS_AUTOSEND = "enable_autosend"; /** * Preference's name: use current connector for autosend. */ private static final String PREFS_USE_CURRENT_CON = "use_current_connector"; /** * Preference's name: vibrate on sending. */ static final String PREFS_SEND_VIBRATE = "send_vibrate"; /** * Preference's name: vibrate on failed sending. */ static final String PREFS_FAIL_VIBRATE = "fail_vibrate"; /** * Preference's name: sound on failed sending. */ static final String PREFS_FAIL_SOUND = "fail_sound"; /** * Preference's name: hide select recipients button. */ private static final String PREFS_HIDE_SELECT_RECIPIENTS_BUTTON = "hide_select_recipients_button"; /** * Preference's name: hide clear recipients button. */ private static final String PREFS_HIDE_CLEAR_RECIPIENTS_BUTTON = "hide_clear_recipients_button"; /** * Preference's name: hide emoticons button. */ private static final String PREFS_HIDE_EMO_BUTTON = "hide_emo_button"; /** * Preference's name: hide cancel button. */ private static final String PREFS_HIDE_CANCEL_BUTTON = "hide_cancel_button"; /** * Preference's name: hide extras button. */ private static final String PREFS_HIDE_EXTRAS_BUTTON = "hide_extras_button"; /** * Preference's name: hide bg connector. */ private static final String PREFS_HIDE_BG_CONNECTOR = "hide_bg_connector"; /** * Preference's name: hide paste button. */ private static final String PREFS_HIDE_PASTE = "hide_paste"; /** * Preference's name: show toast on balance update. */ static final String PREFS_SHOW_BALANCE_TOAST = "show_balance_toast"; /** * Cache {@link ConnectorSpec}s. */ private static final String PREFS_CONNECTORS = "connectors"; /** * Preference's name: try to send invalid characters. */ private static final String PREFS_TRY_SEND_INVALID = "try_send_invalid"; /** * Preference's name: drop sent messages. */ static final String PREFS_DROP_SENT = "drop_sent"; /** * Preference's name: backup of last sms. */ private static final String PREFS_BACKUPLASTTEXT = "backup_last_sms"; /** * Preference's name: default recipient. */ private static final String PREFS_DEFAULT_RECIPIENT = "default_recipient"; /** * Preference's name: signature. */ private static final String PREFS_SIGNATURE = "signature"; /** * Preference's name: max resend count. */ static final String PREFS_MAX_RESEND_COUNT = "max_resend_count"; /** * Preference's name: internal id of the last message. */ static final String PREFS_LAST_MSG_ID = "last_msg_id"; /** * Preference's name: last time help intro was shown. */ private static final String PREFS_LASTHELP = "last_help"; /** * Preference's name: selected {@link ConnectorSpec} ID. */ static final String PREFS_CONNECTOR_ID = "connector_id"; /** * Preference's name: selected {@link SubConnectorSpec} ID. */ static final String PREFS_SUBCONNECTOR_ID = "subconnector_id"; /** * Preference's name: standard connector. */ static final String PREFS_STANDARD_CONNECTOR = "std_connector"; /** * Preference's name: standard sub connector. */ static final String PREFS_STANDARD_SUBCONNECTOR = "std_subconnector"; /** * Preference's name: to. */ private static final String EXTRA_TO = "to"; /** * Preference's name: text. */ private static final String EXTRA_TEXT = "text"; /** * Sleep before autoexit. */ private static final int SLEEP_BEFORE_EXIT = 75; /** * Buffersize for saving and loading Connectors. */ private static final int BUFSIZE = 4096; /** * Minimum length for showing sms length. */ private static final int TEXT_LABLE_MIN_LEN = 20; /** * Preferences: selected {@link ConnectorSpec}. */ private static ConnectorSpec prefsConnectorSpec = null; /** * Preferences: selected {@link SubConnectorSpec}. */ private static SubConnectorSpec prefsSubConnectorSpec = null; /** * List of available {@link ConnectorSpec}s. */ private static final ArrayList<ConnectorSpec> CONNECTORS = new ArrayList<ConnectorSpec>(); /** * List of available pseudo-connector. */ private static final List<ConnectorSpec> PSEUDO_CONNECTORS = new ArrayList<ConnectorSpec>(); private static PseudoConnectorRules rules = new PseudoConnectorRules(); /** * true if preferences got opened. */ static boolean doPreferences = false; /** * Menu item: restore. */ private static final int ITEM_RESTORE = 1; /** * Dialog: custom sender. */ private static final int DIALOG_CUSTOMSENDER = 3; /** * Dialog: send later: date. */ private static final int DIALOG_SENDLATER_DATE = 4; /** * Dialog: send later: time. */ private static final int DIALOG_SENDLATER_TIME = 5; /** * Dialog: emo. */ private static final int DIALOG_EMO = 6; /** * {@link Activity} result request. */ private static final int ARESULT_PICK_PHONE = 1; /** * Size of the emoticons png. */ private static final int EMOTICONS_SIZE = 50; /** * Padding for the emoticons png. */ private static final int EMOTICONS_PADDING = 5; /** * Intent's extra for error messages. */ static final String EXTRA_ERRORMESSAGE = "de.ub0r.android.intent.extra.ERRORMESSAGE"; /** * Intent's extra for sending message automatically. */ static final String EXTRA_AUTOSEND = "AUTOSEND"; /** * Persistent Message store. */ private String lastMsg = null; /** * Persistent Recipient store. */ private String lastTo = null; /** * Backup for params: custom sender. */ private static String lastCustomSender = null; /** * Backup for params: send later. */ private static long lastSendLater = -1; /** * {@link MultiAutoCompleteTextView} holding recipients. */ private MultiAutoCompleteTextView etTo; /** * {@link EditText} holding text. */ private EditText etText; /** * {@link TextView} for pasting text. */ private TextView tvPaste; /** * {@link TextView} for deleting text. */ private TextView tvClear; /** * {@link View} holding custom sender. */ private ToggleButton vCustomSender; /** * {@link View} holding flashsms. */ private ToggleButton vFlashSMS; /** * {@link View} holding send later. */ private ToggleButton vSendLater; /** * {@link ClipboardManager}. */ private ClipboardManager cbmgr; /** * Text's label. */ private TextView etTextLabel; /** * Show cancel button. */ private static boolean prefsShowCancel = true; /** * An estimate of the number of connectors that are remaining to be added. */ private static int newConnectorsExpected = 0; private Handler threadHandler; /** * TextWatcher en-/disable send/cancel buttons. */ private TextWatcher twButtons = new TextWatcher() { /** * {@inheritDoc} */ public void afterTextChanged(final Editable s) { final boolean b1 = WebSMS.this.etTo.getText().length() > 0; final boolean b2 = WebSMS.this.etText.getText().length() > 0; WebSMS.this.findViewById(R.id.clear).setEnabled(b1); int v = View.GONE; if (prefsShowCancel && (b1 || b2)) { v = View.VISIBLE; } WebSMS.this.tvClear.setVisibility(v); WebSMS.this.invalidateOptionsMenu(); } /** Needed dummy. */ public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) { } /** Needed dummy. */ public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { } }; /** * TextWatcher updating char count on writing. */ private TextWatcher twCount = new TextWatcher() { /** * {@inheritDoc} */ @SuppressWarnings("deprecation") public void afterTextChanged(final Editable s) { int len = s.length(); if (len == 0) { WebSMS.this.etTextLabel.setVisibility(View.GONE); if (WebSMS.this.cbmgr.hasText() && !PreferenceManager.getDefaultSharedPreferences(WebSMS.this) .getBoolean(PREFS_HIDE_PASTE, false)) { WebSMS.this.tvPaste.setVisibility(View.VISIBLE); } else { WebSMS.this.tvPaste.setVisibility(View.GONE); } } else { final String sig = PreferenceManager.getDefaultSharedPreferences(WebSMS.this) .getString(PREFS_SIGNATURE, ""); len += sig.length(); WebSMS.this.tvPaste.setVisibility(View.GONE); if (len > TEXT_LABLE_MIN_LEN) { SMSLengthCalculator calc = null; if (prefsConnectorSpec != null) { calc = prefsConnectorSpec.getSMSLengthCalculator(); } if (calc == null) { calc = SMS_LENGTH_CALCULATOR; } int[] l = calc.calculateLength(s.toString() + sig, false); WebSMS.this.etTextLabel.setText(l[0] + "/" + l[2]); WebSMS.this.etTextLabel.setVisibility(View.VISIBLE); } else { WebSMS.this.etTextLabel.setVisibility(View.GONE); } // If we have a connector selected, check message length limit if (prefsConnectorSpec != null) { // Get the limit, will be -1 or 0 if it is not set int maxLength = prefsConnectorSpec.getLimitLength(); if (maxLength > 0 && len > maxLength) { // Truncate to maxLength-sig.length() chars int actualMax = maxLength - sig.length(); String newText = s.toString().substring(0, actualMax); Log.i(TAG, "Message text was too long, so truncating from " + s.length() + " to " + newText.length()); s.replace(0, s.length(), newText); if (me != null) { String sigText = sig.length() > 0 ? me.getString(R.string.connector_message_length_reached_signature, sig.length()) : ""; String messageText = me.getString(R.string.connector_message_length_reached, maxLength, prefsConnectorSpec.getName(), sigText); Toast.makeText(me, messageText, Toast.LENGTH_SHORT).show(); } } } } } /** Needed dummy. */ public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) { } /** Needed dummy. */ public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { } }; /** * Show extra button. */ private static boolean bShowExtras = true; /** * Parse data pushed by {@link Intent}. * * @param intent {@link Intent} */ private void parseIntent(final Intent intent) { final String action = intent.getAction(); Log.d(TAG, "launched with action: " + action); if (action == null) { return; } final Uri uri = intent.getData(); Log.i(TAG, "launched with uri: " + uri); if (uri != null && uri.toString().length() > 0) { // launched by clicking a sms: link, target number is in URI. final String scheme = uri.getScheme(); if (scheme != null) { if (scheme.equals("sms") || scheme.equals("smsto")) { final String s = uri.getSchemeSpecificPart(); this.parseSchemeSpecificPart(s); } else if (scheme.equals("content")) { this.parseThreadId(uri.getLastPathSegment()); } } } // check for extras String s = intent.getStringExtra("address"); if (!TextUtils.isEmpty(s)) { Log.d(TAG, "got address: " + s); this.lastTo = s; } s = intent.getStringExtra(Intent.EXTRA_TEXT); if (s == null) { Log.d(TAG, "got sms_body: " + s); s = intent.getStringExtra("sms_body"); } if (s == null) { final Uri stream = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM); if (stream != null) { Log.d(TAG, "got stream: " + stream); try { InputStream is = this.getContentResolver().openInputStream(stream); final BufferedReader r = new BufferedReader(new InputStreamReader(is)); StringBuffer sb = new StringBuffer(); String line; while ((line = r.readLine()) != null) { sb.append(line + "\n"); } s = sb.toString().trim(); } catch (IOException e) { Log.e(TAG, "IO ERROR", e); } } } if (s != null) { Log.d(TAG, "set text: " + s); ((EditText) this.findViewById(R.id.text)).setText(s); this.lastMsg = s; } s = intent.getStringExtra(EXTRA_ERRORMESSAGE); if (s != null) { Log.e(TAG, "show error: " + s); Toast.makeText(this, s, Toast.LENGTH_LONG).show(); } final SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(this); if (p.getBoolean(PREFS_AUTOSEND, true)) { s = intent.getStringExtra(WebSMS.EXTRA_AUTOSEND); Log.d(TAG, "try autosend.."); Log.d(TAG, "s: " + s); Log.d(TAG, "lastMsg: " + this.lastMsg); Log.d(TAG, "lastTo: " + this.lastTo); if (s != null && !TextUtils.isEmpty(this.lastMsg) && !TextUtils.isEmpty(this.lastTo)) { // all data is here Log.d(TAG, "do autosend"); if (p.getBoolean(PREFS_USE_CURRENT_CON, true)) { // push it to current active connector Log.d(TAG, "use current connector"); if (prefsConnectorSpec != null && prefsSubConnectorSpec != null) { Log.d(TAG, "autosend: call send()"); if (this.send(prefsConnectorSpec, prefsSubConnectorSpec) && !this.isFinishing()) { Log.d(TAG, "sent successfully"); this.finish(); } } } else { // show connector chooser Log.d(TAG, "show connector chooser"); final AlertDialog.Builder b = new AlertDialog.Builder(this); b.setTitle(R.string.change_connector_); final ConnectorLabel[] items = this.getConnectorMenuItems(true /*isIncludePseudoConnectors*/); Log.d(TAG, "show #items: " + items.length); if (items.length > 0) { b.setItems(items, new DialogInterface.OnClickListener() { @Override public void onClick(final DialogInterface dialog, final int which) { final ConnectorLabel sel = items[which]; // save old selected connector final ConnectorSpec pr0 = prefsConnectorSpec; final SubConnectorSpec pr1 = prefsSubConnectorSpec; // switch to selected WebSMS.this.saveSelectedConnector(sel.getConnector(), sel.getSubConnector()); // send message boolean sent = false; Log.d(TAG, "autosend: call send()"); if (prefsConnectorSpec != null && prefsSubConnectorSpec != null) { sent = WebSMS.this.send(prefsConnectorSpec, prefsSubConnectorSpec); } // restore old connector WebSMS.this.saveSelectedConnector(pr0, pr1); // quit if (sent && !WebSMS.this.isFinishing()) { Log.d(TAG, "sent successfully"); WebSMS.this.finish(); } } }); b.setNegativeButton(android.R.string.cancel, null); b.show(); } } } } } /** * parseSchemeSpecificPart from {@link Uri} and initialize WebSMS * properties. * * @param part scheme specific part */ private void parseSchemeSpecificPart(final String part) { Log.d(TAG, "parseSchemeSpecificPart(" + part + ")"); String s = part; if (s == null) { return; } s = s.trim(); if (s.endsWith(",")) { s = s.substring(0, s.length() - 1).trim(); } if (s.indexOf('<') < 0) { // try to fetch recipient's name from phone book String n = ContactsWrapper.getInstance().getNameForNumber(this.getContentResolver(), s); if (n != null) { s = n + " <" + s + ">, "; } } Log.d(TAG, "parseSchemeSpecificPart(" + part + "): " + s); ((EditText) this.findViewById(R.id.to)).setText(s); this.lastTo = s; } /** * Load data from Conversation. * * @param threadId ThreadId */ private void parseThreadId(final String threadId) { Log.d(TAG, "thradId: " + threadId); final Uri uri = Uri.parse("content://mms-sms/conversations/" + threadId); final String[] proj = new String[] { "thread_id", "address" }; Cursor cursor = null; try { try { cursor = this.getContentResolver().query(uri, proj, null, null, null); } catch (SQLException e) { Log.e(TAG, null, e); proj[0] = "_id"; proj[1] = "recipient_address"; cursor = this.getContentResolver().query(uri, proj, null, null, null); } if (cursor != null && cursor.moveToFirst()) { String a; do { a = cursor.getString(1); } while (a == null && cursor.moveToNext()); Log.d(TAG, "found address: " + a); this.parseSchemeSpecificPart(a); } } catch (IllegalStateException e) { Log.e(TAG, "error parsing ThreadId: " + threadId, e); } if (cursor != null && !cursor.isClosed()) { cursor.close(); } } /** * {@inheritDoc} */ @SuppressWarnings({ "unchecked", "deprecation" }) @Override public final void onCreate(final Bundle savedInstanceState) { this.requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); this.setTheme(PreferencesActivity.getTheme(this)); super.onCreate(savedInstanceState); Log.d(TAG, "onCreate(" + savedInstanceState + ")"); this.threadHandler = new Handler(); // Restore preferences de.ub0r.android.lib.Utils.setLocale(this); this.cbmgr = (ClipboardManager) this.getSystemService(CLIPBOARD_SERVICE); // save ref to me. me = this; final SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(this); // inflate XML this.setContentView(R.layout.main); this.getSupportActionBar().setHomeButtonEnabled(true); // indeterminate progress bar is spinning by default so stop it, // updateProgressBar will start it again if necessary this.setSupportProgressBarIndeterminateVisibility(false); this.etTo = (MultiAutoCompleteTextView) this.findViewById(R.id.to); this.etText = (EditText) this.findViewById(R.id.text); this.etTextLabel = (TextView) this.findViewById(R.id.text_); this.tvPaste = (TextView) this.findViewById(R.id.text_paste); this.tvClear = (TextView) this.findViewById(R.id.text_clear); this.vCustomSender = (ToggleButton) this.findViewById(R.id.custom_sender); this.vFlashSMS = (ToggleButton) this.findViewById(R.id.flashsms); this.vSendLater = (ToggleButton) this.findViewById(R.id.send_later); if (isNewVersion()) { Log.i(TAG, "detected version update"); SharedPreferences.Editor editor = p.edit(); editor.remove(PREFS_CONNECTORS); // remove cache editor.apply(); rules.upgrade(); } // get cached Connectors String s = p.getString(PREFS_CONNECTORS, null); if (TextUtils.isEmpty(s)) { this.updateConnectors(); } else if (CONNECTORS.size() == 0) { // skip static remaining connectors try { ArrayList<ConnectorSpec> cache; cache = (ArrayList<ConnectorSpec>) (new ObjectInputStream(new BufferedInputStream( new ByteArrayInputStream(Base64.decode(s, Base64.DEFAULT)), BUFSIZE))).readObject(); CONNECTORS.addAll(cache); if (p.getBoolean(PREFS_AUTOUPDATE, true)) { updateFreecount(); } } catch (Exception e) { Log.d(TAG, "error loading connectors", e); } } Log.d(TAG, "loaded connectors: " + CONNECTORS.size()); if (PSEUDO_CONNECTORS.size() == 0) { PSEUDO_CONNECTORS.add(rules.getSpec(this)); } if (savedInstanceState == null) { this.revertPrefsToStdConnector(); // note: do not revert to std connector on orientation change } this.reloadPrefs(); if (savedInstanceState != null) { this.lastTo = savedInstanceState.getString(EXTRA_TO); this.lastMsg = savedInstanceState.getString(EXTRA_TEXT); } // register Listener this.vCustomSender.setOnClickListener(this); this.vSendLater.setOnClickListener(this); this.findViewById(R.id.select).setOnClickListener(this); View v = this.findViewById(R.id.clear); v.setOnClickListener(this); v.setOnLongClickListener(this); this.findViewById(R.id.emo).setOnClickListener(this); this.tvPaste.setOnClickListener(this); this.tvClear.setOnClickListener(this); this.etText.addTextChangedListener(this.twCount); this.etText.addTextChangedListener(this.twButtons); this.etTo.addTextChangedListener(this.twButtons); this.etTo.setAdapter(new MobilePhoneAdapter(this)); this.etTo.setTokenizer(new MultiAutoCompleteTextView.CommaTokenizer()); this.etTo.requestFocus(); this.parseIntent(this.getIntent()); boolean checkPrefix = true; boolean showIntro = false; if (TextUtils.isEmpty(p.getString(PREFS_SENDER, null)) && TextUtils.isEmpty(p.getString(PREFS_DEFPREFIX, null)) && CONNECTORS.size() == 0) { checkPrefix = false; showIntro = true; } requestPermission(Manifest.permission.READ_CONTACTS, PERMISSIONS_REQUEST_READ_CONTACTS, R.string.permissions_read_contacts, null); if (TextUtils.isEmpty(p.getString(PREFS_SENDER, null)) || TextUtils.isEmpty(p.getString(PREFS_DEFPREFIX, null))) { fetchSenderAndPrefixFromPhoneNumber(); } // check default prefix if (checkPrefix && !p.getString(PREFS_DEFPREFIX, "").startsWith("+")) { this.log(R.string.log_wrong_defprefix); } if (showIntro) { // skip help intro for at least 2min if (System.currentTimeMillis() > p.getLong(PREFS_LASTHELP, 0L) + de.ub0r.android.lib.Utils.MINUTES_IN_MILLIS * 2) { p.edit().putLong(PREFS_LASTHELP, System.currentTimeMillis()).apply(); this.startActivity(new Intent(this, HelpIntroActivity.class)); } } } @Override public void onRequestPermissionsResult(final int requestCode, @NonNull final String permissions[], @NonNull final int[] grantResults) { switch (requestCode) { case PERMISSIONS_REQUEST_READ_PHONE_STATE: { // ignore denied permission for now, user might set up sender/prefix by hand if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // just try again. fetchSenderAndPrefixFromPhoneNumber(); } return; } case PERMISSIONS_REQUEST_SEND_SMS: { // ignore denied permission for now, user is unable to send sms.. if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // just try again. send(prefsConnectorSpec, prefsSubConnectorSpec); } return; } } } private void fetchSenderAndPrefixFromPhoneNumber() { final TelephonyManager tm = (TelephonyManager) this.getSystemService(TELEPHONY_SERVICE); if (tm == null) { return; } if (!requestPermission(android.Manifest.permission.READ_PHONE_STATE, PERMISSIONS_REQUEST_READ_PHONE_STATE, R.string.permissions_read_phone_state, null)) { return; } String number = tm.getLine1Number(); Log.i(TAG, "line1: " + number); if (number != null && number.startsWith("00")) { number = number.replaceFirst("00", "+"); } if (number != null && !TextUtils.isEmpty(number) && (number.startsWith("+"))) { final SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(this); final Editor e = p.edit(); if (TextUtils.isEmpty(p.getString(PREFS_SENDER, null))) { Log.i(TAG, "set number=" + number); e.putString(PREFS_SENDER, number); } if (TextUtils.isEmpty(p.getString(PREFS_DEFPREFIX, null))) { String prefix = de.ub0r.android.lib.Utils.getPrefixFromTelephoneNumber(number); if (!TextUtils.isEmpty(prefix)) { Log.i(TAG, "set prefix=" + prefix); e.putString(PREFS_DEFPREFIX, prefix); } else { Log.w(TAG, "unable to get prefix from number: " + number); } } e.apply(); } } private boolean requestPermission(final String permission, final int requestCode, final int message, final DialogInterface.OnClickListener onCancelListener) { Log.i(TAG, "requesting permission: " + permission); if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) { if (ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) { new Builder(this).setTitle(R.string.permissions_).setMessage(message).setCancelable(false) .setNegativeButton(android.R.string.cancel, onCancelListener) .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(final DialogInterface dialogInterface, final int i) { ActivityCompat.requestPermissions(WebSMS.this, new String[] { permission }, requestCode); } }).show(); } else { ActivityCompat.requestPermissions(this, new String[] { permission }, requestCode); } return false; } else { return true; } } private boolean isNewVersion() { SharedPreferences p = getPreferences(MODE_PRIVATE); if (BuildConfig.VERSION_CODE != p.getInt(LAST_RUN, 0)) { p.edit().putInt(LAST_RUN, BuildConfig.VERSION_CODE).apply(); return true; } return false; } @Override protected final void onSaveInstanceState(final Bundle outState) { super.onSaveInstanceState(outState); outState.putString(EXTRA_TO, this.lastTo); outState.putString(EXTRA_TEXT, this.lastMsg); } /** * {@inheritDoc} */ @Override protected final void onActivityResult(final int requestCode, final int resultCode, final Intent data) { if (requestCode == ARESULT_PICK_PHONE) { if (resultCode == RESULT_OK) { final Uri u = data.getData(); if (u == null) { return; } try { final String phone = ContactsWrapper.getInstance().getNameAndNumber(this.getContentResolver(), u) + ", "; String t = null; if (this.etTo != null) { t = this.etTo.getText().toString().trim(); } if (TextUtils.isEmpty(t) && !TextUtils.isEmpty(this.lastTo)) { t = this.lastTo.trim(); } if (TextUtils.isEmpty(t)) { t = phone; } else if (t.endsWith(",")) { t += " " + phone; } else { t += ", " + phone; } this.lastTo = t; this.etTo.setText(t); } catch (IllegalStateException e) { Log.e(TAG, "failed resolving name and number", e); } } } } @Override protected final void onNewIntent(final Intent intent) { super.onNewIntent(intent); Log.d(TAG, "onNewIntent(" + intent + ")"); this.parseIntent(intent); } /** * Update {@link ConnectorSpec}s. */ private void updateConnectors() { // query for connectors final Intent i = new Intent(Connector.ACTION_CONNECTOR_UPDATE); i.setFlags(i.getFlags() | Intent.FLAG_INCLUDE_STOPPED_PACKAGES); Log.d(TAG, "send broadcast: " + i.getAction()); newConnectorsExpected = this.getInstalledConnectorsCount() - CONNECTORS.size(); updateProgressBar(); this.sendBroadcast(i); } /** * {@inheritDoc} */ @Override protected final void onResume() { super.onResume(); // set accounts' balance to gui this.updateBalance(); // if coming from prefs.. if (doPreferences) { this.reloadPrefs(); this.updateConnectors(); doPreferences = false; final SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(this); final String defPrefix = p.getString(PREFS_DEFPREFIX, "+49"); final String defSender = p.getString(PREFS_SENDER, ""); final ConnectorSpec[] css = getConnectors(ConnectorSpec.CAPABILITIES_BOOTSTRAP, ConnectorSpec.STATUS_ENABLED | ConnectorSpec.STATUS_READY, false /*isIncludePseudoConnectors*/); for (ConnectorSpec cs : css) { runCommand(this, cs, ConnectorCommand.bootstrap(defPrefix, defSender)); } } else { // check is count of connectors changed final int s1 = this.getInstalledConnectorsCount(); final int s2 = CONNECTORS.size(); if (s1 != s2) { Log.d(TAG, "clear connector cache (" + s1 + "/" + s2 + ")"); CONNECTORS.clear(); this.updateConnectors(); } } // get updated specs for pseudo-connectors rules.updateSpec(this); // this will update the singleton spec this.setButtons(); if (this.lastTo == null || this.lastTo.length() == 0) { final SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(this); this.lastTo = p.getString(PREFS_DEFAULT_RECIPIENT, null); } // reload text/recipient from local store if (TextUtils.isEmpty(this.etText.getText())) { if (this.lastMsg != null) { this.etText.setText(this.lastMsg); } else { this.etText.setText(""); } } if (TextUtils.isEmpty(this.etTo.getText())) { if (this.lastTo != null) { this.etTo.setText(this.lastTo); } else { this.etTo.setText(""); } } if (this.lastTo != null && this.lastTo.length() > 0) { this.etText.requestFocus(); this.etText.setSelection(this.etText.getText().length()); } else { this.etTo.requestFocus(); } } /** * Update balance. */ private void updateBalance() { Log.d(TAG, "updateBalance()"); final StringBuilder buf = new StringBuilder(); final ConnectorSpec[] css = getConnectors(ConnectorSpec.CAPABILITIES_UPDATE, ConnectorSpec.STATUS_ENABLED, false /*isIncludePseudoConnectors*/); String singleb = null; for (ConnectorSpec cs : css) { final String b = cs.getBalance(); if (b == null || b.length() == 0) { continue; } if (buf.length() > 0) { buf.append(", "); singleb = null; } else { singleb = b; } buf.append(cs.getName()); buf.append(": "); buf.append(b); } if (singleb != null) { buf.replace(0, buf.length(), singleb); } this.getSupportActionBar().setSubtitle(buf.toString()); } /** * {@inheritDoc} */ @Override protected final void onPause() { // store input data to persistent stores this.lastMsg = this.etText.getText().toString(); this.lastTo = this.etTo.getText().toString(); this.savePreferences(); super.onPause(); } @Override protected final void onDestroy() { final Editor editor = PreferenceManager.getDefaultSharedPreferences(this).edit(); try { final ByteArrayOutputStream out = new ByteArrayOutputStream(); ObjectOutputStream objOut = new ObjectOutputStream(new BufferedOutputStream(out, BUFSIZE)); objOut.writeObject(CONNECTORS); objOut.close(); final String s = Base64.encodeToString(out.toByteArray(), Base64.DEFAULT); Log.d(TAG, s); editor.putString(PREFS_CONNECTORS, s); } catch (Exception e) { editor.remove(PREFS_CONNECTORS); Log.e(TAG, "IO", e); } editor.apply(); super.onDestroy(); } /** * Read static variables holding preferences. */ private void reloadPrefs() { Log.d(TAG, "reloadPrefs()"); int ts = PreferencesActivity.getTextsize(this); if (ts != 0) { this.etTo.setTextSize(ts); this.etText.setTextSize(ts); } final SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(this); final boolean bShowEmoticons = !p.getBoolean(PREFS_HIDE_EMO_BUTTON, false); prefsShowCancel = !p.getBoolean(PREFS_HIDE_CANCEL_BUTTON, false); bShowExtras = !p.getBoolean(PREFS_HIDE_EXTRAS_BUTTON, false); final boolean bShowClearRecipients = !p.getBoolean(PREFS_HIDE_CLEAR_RECIPIENTS_BUTTON, false); final boolean bShowSelectRecipients = !p.getBoolean(PREFS_HIDE_SELECT_RECIPIENTS_BUTTON, false); View v = this.findViewById(R.id.select); if (bShowSelectRecipients) { v.setVisibility(View.VISIBLE); } else { v.setVisibility(View.GONE); } v = this.findViewById(R.id.clear); if (bShowClearRecipients) { v.setVisibility(View.VISIBLE); } else { v.setVisibility(View.GONE); } v = this.findViewById(R.id.emo); if (bShowEmoticons) { v.setVisibility(View.VISIBLE); } else { v.setVisibility(View.GONE); } v = this.findViewById(R.id.text_connector); if (p.getBoolean(PREFS_HIDE_BG_CONNECTOR, false)) { v.setVisibility(View.INVISIBLE); } else { v.setVisibility(View.VISIBLE); } String prefsConnectorID = p.getString(PREFS_CONNECTOR_ID, ""); ConnectorSpec cs = getConnectorByID(prefsConnectorID); if (cs != null && cs.hasStatus(ConnectorSpec.STATUS_ENABLED)) { prefsConnectorSpec = cs; String prefsSubConnectorID = p.getString(PREFS_SUBCONNECTOR_ID, ""); prefsSubConnectorSpec = cs.getSubConnector(prefsSubConnectorID); if (prefsSubConnectorSpec == null) { prefsSubConnectorSpec = cs.getSubConnectors()[0]; } } else { ConnectorSpec[] connectors = getConnectors(ConnectorSpec.CAPABILITIES_SEND, ConnectorSpec.STATUS_ENABLED, true /*isIncludePseudoConnectors*/); if (connectors.length == 1 && connectors[0].getSubConnectors().length == 1) { prefsConnectorSpec = connectors[0]; prefsSubConnectorSpec = prefsConnectorSpec.getSubConnectors()[0]; Toast.makeText(this, this.getString(R.string.connectors_switch) + " " + prefsConnectorSpec.getName(), Toast.LENGTH_LONG).show(); } else { prefsConnectorSpec = null; prefsSubConnectorSpec = null; } } MobilePhoneAdapter.setMobileNumbersOnly(p.getBoolean(PREFS_MOBILES_ONLY, false)); this.setButtons(); } /** * Updates preferences and replaces the selected connector with the default (standard) one. * reloadPrefs() should be called afterwards to load the change. */ private void revertPrefsToStdConnector() { final SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(this); String stdConnector = p.getString(PREFS_STANDARD_CONNECTOR, ""); String stdSubConnector = p.getString(PREFS_STANDARD_SUBCONNECTOR, ""); if (!TextUtils.isEmpty(stdConnector)) { p.edit().putString(PREFS_CONNECTOR_ID, stdConnector).putString(PREFS_SUBCONNECTOR_ID, stdSubConnector) .apply(); } } /** * Show/hide, enable/disable send buttons. */ private void setButtons() { if (prefsConnectorSpec != null && prefsSubConnectorSpec != null && prefsConnectorSpec.hasStatus(ConnectorSpec.STATUS_ENABLED)) { final boolean sFlashsms = prefsSubConnectorSpec.hasFeatures(SubConnectorSpec.FEATURE_FLASHSMS); final boolean sCustomsender = prefsSubConnectorSpec.hasFeatures(SubConnectorSpec.FEATURE_CUSTOMSENDER); final boolean sSendLater = prefsSubConnectorSpec.hasFeatures(SubConnectorSpec.FEATURE_SENDLATER); if (bShowExtras && (sFlashsms || sCustomsender || sSendLater)) { if (sFlashsms) { this.vFlashSMS.setVisibility(View.VISIBLE); } else { this.vFlashSMS.setVisibility(View.GONE); } if (sCustomsender) { this.vCustomSender.setVisibility(View.VISIBLE); this.vCustomSender.setChecked(!TextUtils.isEmpty(lastCustomSender)); } else { this.vCustomSender.setVisibility(View.GONE); this.vCustomSender.setChecked(false); } if (sSendLater) { this.vSendLater.setVisibility(View.VISIBLE); } else { this.vSendLater.setVisibility(View.GONE); } this.findViewById(R.id.extraButtons).setVisibility(View.VISIBLE); } else { this.findViewById(R.id.extraButtons).setVisibility(View.GONE); } String t = prefsConnectorSpec.getName(); if (prefsConnectorSpec.getSubConnectorCount() > 1) { t += " - " + prefsSubConnectorSpec.getName(); } this.setTitle(t); String s = t; if (lastSendLater > 0L) { Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(lastSendLater); s += "\n@" + DateFormat.getDateFormat(this).format(cal.getTime()); s += " " + DateFormat.getTimeFormat(this).format(cal.getTime()); this.vSendLater.setChecked(true); } else { this.vSendLater.setChecked(false); } Log.d(TAG, "set backgroundtext: " + s); ((TextView) this.findViewById(R.id.text_connector)).setText(s); } else { this.setTitle(R.string.app_name); ((TextView) this.findViewById(R.id.text_connector)).setText(""); if (CONNECTORS.size() > 0) { Toast.makeText(this, R.string.log_noselectedconnector, Toast.LENGTH_SHORT).show(); } this.findViewById(R.id.extraButtons).setVisibility(View.GONE); } } /** * Resets persistent store. * * @param backupText backup text to {@link SharedPreferences} */ private void reset(final boolean backupText) { this.lastMsg = this.etText.getText().toString(); // save user preferences final SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(this).edit(); if (backupText) { if (!TextUtils.isEmpty(this.lastMsg)) { editor.putString(PREFS_BACKUPLASTTEXT, this.lastMsg); } } else { editor.remove(PREFS_BACKUPLASTTEXT); } editor.apply(); this.etText.setText(""); this.etTo.setText(""); this.lastMsg = null; this.lastTo = null; lastCustomSender = null; lastSendLater = -1; this.setButtons(); } /** * Save prefs. */ final void savePreferences() { if (prefsConnectorSpec != null && prefsSubConnectorSpec != null) { PreferenceManager.getDefaultSharedPreferences(this).edit() .putString(PREFS_CONNECTOR_ID, prefsConnectorSpec.getPackage()) .putString(PREFS_SUBCONNECTOR_ID, prefsSubConnectorSpec.getID()).commit(); } } /** * Run Connector.doUpdate(). */ private void updateFreecount() { final SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(this); final String defPrefix = p.getString(PREFS_DEFPREFIX, "+49"); final String defSender = p.getString(PREFS_SENDER, ""); final ConnectorSpec[] css = getConnectors(ConnectorSpec.CAPABILITIES_UPDATE, ConnectorSpec.STATUS_ENABLED | ConnectorSpec.STATUS_READY, false /*isIncludePseudoConnectors*/); for (ConnectorSpec cs : css) { if (cs.isRunning()) { // skip running connectors Log.d(TAG, "skip running connector: " + cs.getName()); continue; } runCommand(this, cs, ConnectorCommand.update(defPrefix, defSender)); } } /** * Send a command as broadcast. * * @param context Current context * @param connector {@link ConnectorSpec} * @param command {@link ConnectorCommand} */ static void runCommand(final Context context, final ConnectorSpec connector, final ConnectorCommand command) { connector.setErrorMessage((String) null); final Intent intent = command.setToIntent(null); short t = command.getType(); boolean sendOrdered = false; switch (t) { case ConnectorCommand.TYPE_BOOTSTRAP: sendOrdered = true; intent.setAction(connector.getPackage() + Connector.ACTION_RUN_BOOTSTRAP); connector.addStatus(ConnectorSpec.STATUS_BOOTSTRAPPING); break; case ConnectorCommand.TYPE_SEND: sendOrdered = true; intent.setAction(connector.getPackage() + Connector.ACTION_RUN_SEND); connector.setToIntent(intent); connector.addStatus(ConnectorSpec.STATUS_SENDING); if (command.getResendCount() == 0) { WebSMSReceiver.saveMessage(me, connector, command, WebSMSReceiver.MESSAGE_TYPE_DRAFT); } break; case ConnectorCommand.TYPE_UPDATE: intent.setAction(connector.getPackage() + Connector.ACTION_RUN_UPDATE); connector.addStatus(ConnectorSpec.STATUS_UPDATING); break; default: break; } updateProgressBar(); intent.setFlags(intent.getFlags() | Intent.FLAG_INCLUDE_STOPPED_PACKAGES); Log.d(TAG, "send broadcast: " + intent.getAction()); if (sendOrdered) { context.sendOrderedBroadcast(intent, null, new BroadcastReceiver() { @Override public void onReceive(final Context context, final Intent intent) { if (this.getResultCode() != Activity.RESULT_OK) { ConnectorCommand command = new ConnectorCommand(intent); ConnectorSpec specs = new ConnectorSpec(intent); specs.setErrorMessage(// TODO: localize "Connector did not react on message"); WebSMSReceiver.handleSendCommand(context, specs, command); } } }, null, Activity.RESULT_CANCELED, null, null); } else { context.sendBroadcast(intent); } } /** * {@inheritDoc} */ @SuppressWarnings("deprecation") public final void onClick(final View v) { CharSequence s; switch (v.getId()) { case R.id.select: this.startActivityForResult(ContactsWrapper.getInstance().getPickPhoneIntent(), ARESULT_PICK_PHONE); return; case R.id.text_clear: this.reset(true); this.revertPrefsToStdConnector(); this.reloadPrefs(); return; case R.id.clear: s = this.etTo.getText(); final String ss = s.toString(); int i = ss.lastIndexOf(","); if (ss.substring(i + 1).trim().length() <= 0) { i = ss.substring(0, i).lastIndexOf(","); } if (i <= 0) { this.lastTo = null; this.etTo.setText(""); } else { this.lastTo = ss.substring(0, i) + ", "; this.etTo.setText(this.lastTo); s = this.etTo.getText(); this.etTo.setSelection(s.length()); this.lastTo = s.toString(); } return; case R.id.custom_sender: if (this.vCustomSender.isChecked()) { this.showDialog(DIALOG_CUSTOMSENDER); } else { lastCustomSender = null; } return; case R.id.send_later: if (this.vSendLater.isChecked()) { this.showDialog(DIALOG_SENDLATER_DATE); } else { lastSendLater = -1; } this.setButtons(); return; case R.id.emo: this.showDialog(DIALOG_EMO); return; case R.id.text_paste: s = this.cbmgr.getText(); this.etText.setText(s); s = this.etText.getText(); this.etText.setSelection(s.length()); this.lastMsg = s.toString(); return; default: return; } } /** * {@inheritDoc} */ @Override public final boolean onLongClick(final View v) { switch (v.getId()) { case R.id.clear: this.lastTo = null; this.etTo.setText(""); return true; default: return false; } } /** * {@inheritDoc} */ @Override public final boolean onCreateOptionsMenu(final Menu menu) { getMenuInflater().inflate(R.menu.menu, menu); return true; } /** * Save selected connector. * * @param cs {@link ConnectorSpec} * @param scs {@link SubConnectorSpec} */ private void saveSelectedConnector(final ConnectorSpec cs, final SubConnectorSpec scs) { prefsConnectorSpec = cs; prefsSubConnectorSpec = scs; this.setButtons(); if (cs == null || scs == null) { return; } // save user preferences PreferenceManager.getDefaultSharedPreferences(this).edit() .putString(PREFS_CONNECTOR_ID, prefsConnectorSpec.getPackage()) .putString(PREFS_SUBCONNECTOR_ID, prefsSubConnectorSpec.getID()).commit(); } /** * Get all enabled {@link ConnectorSpec}s as name. * * @param isIncludePseudoConnectors whether pseudo connectors should be included * @return array of {@link ConnectorLabel}. */ public static ConnectorLabel[] getConnectorMenuItems(boolean isIncludePseudoConnectors) { final ConnectorSpec[] css = getConnectors(ConnectorSpec.CAPABILITIES_SEND, ConnectorSpec.STATUS_ENABLED, isIncludePseudoConnectors); final List<ConnectorLabel> items = new ArrayList<>(css.length * 2); SubConnectorSpec[] scs; for (ConnectorSpec cs : css) { scs = cs.getSubConnectors(); if (scs.length == 1) { items.add(new ConnectorLabel(cs, scs[0], true /*isSingleSubConnector*/)); } else { for (SubConnectorSpec sc : scs) { items.add(new ConnectorLabel(cs, sc, false /*isSingleSubConnector*/)); } } } return items.toArray(new ConnectorLabel[items.size()]); } /** * Display "change connector" menu. */ private void changeConnectorMenu() { Log.d(TAG, "changeConnectorMenu()"); AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setIcon(android.R.drawable.ic_menu_share); builder.setTitle(R.string.change_connector_); final ConnectorLabel[] items = this.getConnectorMenuItems(true /*isIncludePseudoConnectors*/); final int l = items.length; if (l == 0) { Toast.makeText(this, R.string.log_noreadyconnector, Toast.LENGTH_LONG).show(); } else if (l == 1) { this.saveSelectedConnector(items[0].getConnector(), items[0].getSubConnector()); } else if (l == 2) { // Find actual connector, pick the other one from css ConnectorLabel newSelected; if (prefsConnectorSpec == null || prefsSubConnectorSpec == null) { newSelected = items[0]; } else if (prefsConnectorSpec.getPackage().equals(items[0].getConnector().getPackage()) && prefsSubConnectorSpec.getID().equals(items[0].getSubConnector().getID())) { newSelected = items[1]; } else { newSelected = items[0]; } this.saveSelectedConnector(newSelected.getConnector(), newSelected.getSubConnector()); Toast.makeText(this, this.getString(R.string.connectors_switch) + " " + newSelected.getName(), Toast.LENGTH_SHORT).show(); } else { builder.setItems(items, new DialogInterface.OnClickListener() { public void onClick(final DialogInterface d, final int idx) { WebSMS.this.saveSelectedConnector(items[idx].getConnector(), items[idx].getSubConnector()); } }); builder.create().show(); } } /** * Save some characters by stripping blanks. */ private void saveChars() { String s = this.etText.getText().toString().trim(); if (s.length() == 0) { return; } String choice = PreferenceManager.getDefaultSharedPreferences(this).getString("save_chars", "remove_spaces"); if (choice.contains("remove_diacritics")) { s = this.removeDiacritics(s); } if (choice.contains("remove_spaces")) { s = this.removeSpaces(s); } this.etText.setText(s); } private String removeSpaces(final String s) { StringBuilder buf = new StringBuilder(); final String[] ss = s.split(" "); for (String ts : ss) { final int l = ts.length(); if (l == 0) { continue; } buf.append(Character.toUpperCase(ts.charAt(0))); if (l == 1) { continue; } buf.append(ts.substring(1)); } return buf.toString(); } @TargetApi(Build.VERSION_CODES.GINGERBREAD) private String removeDiacritics(final String s) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) { return s; } String text = Normalizer.normalize(s, Normalizer.Form.NFD); text = text.replaceAll("[^\\p{ASCII}]", ""); return text; } /** * {@inheritDoc} */ @Override public final boolean onPrepareOptionsMenu(final Menu menu) { final ConnectorSpec[] connectors = getConnectors(ConnectorSpec.CAPABILITIES_SEND, ConnectorSpec.STATUS_READY | ConnectorSpec.STATUS_ENABLED, true /*isIncludePseudoConnectors*/); boolean isPrefsConnectorOk = prefsConnectorSpec != null && prefsSubConnectorSpec != null && prefsConnectorSpec.hasStatus(ConnectorSpec.STATUS_ENABLED); menu.findItem(R.id.item_connector) .setVisible(connectors.length > 1 || (connectors.length == 1 && connectors[0].getSubConnectorCount() > 1) || (connectors.length == 1 && !isPrefsConnectorOk)); boolean hasText = this.etText != null && !TextUtils.isEmpty(this.etText.getText()); menu.findItem(R.id.item_savechars).setVisible(hasText); // only allow to save drafts on API18- menu.findItem(R.id.item_draft).setVisible(Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT && hasText); final boolean showRestore = !TextUtils .isEmpty(PreferenceManager.getDefaultSharedPreferences(this).getString(PREFS_BACKUPLASTTEXT, null)); try { menu.removeItem(ITEM_RESTORE); } catch (Exception e) { Log.w(TAG, "error removing item: " + ITEM_RESTORE, e); } if (showRestore) { menu.add(0, ITEM_RESTORE, android.view.Menu.NONE, R.string.restore_); menu.findItem(ITEM_RESTORE).setIcon(android.R.drawable.ic_menu_revert); } return true; } /** * {@inheritDoc} */ @Override public final boolean onOptionsItemSelected(final MenuItem item) { Log.d(TAG, "onOptionsItemSelected(" + item.getItemId() + ")"); switch (item.getItemId()) { case R.id.item_send: Log.d(TAG, "send button clicked"); this.send(prefsConnectorSpec, prefsSubConnectorSpec); return true; case R.id.item_draft: this.saveDraft(); return true; case R.id.item_savechars: this.saveChars(); return true; case R.id.item_settings: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { this.startActivity(new Intent(this, Preferences11Activity.class)); } else { this.startActivity(new Intent(this, PreferencesActivity.class)); } return true; case R.id.item_connector: this.changeConnectorMenu(); return true; case R.id.item_update: this.updateFreecount(); return true; case android.R.id.home: String s = this.getSupportActionBar().getSubtitle().toString(); if (s.contains(",")) { Builder b = new Builder(this); String bs = this.getString(R.string.free_); b.setTitle(bs.replaceAll(":", "")); b.setMessage(this.getSupportActionBar().getSubtitle().toString().replace(bs, "") .replaceAll(", ", "\n").trim()); b.setCancelable(true); b.show(); return true; } else { return false; } case ITEM_RESTORE: final SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(this); s = p.getString(PREFS_BACKUPLASTTEXT, null); if (!TextUtils.isEmpty(s)) { this.etText.setText(s); } p.edit().remove(PREFS_BACKUPLASTTEXT).apply(); return true; default: return false; } } /** * Create a Emoticons {@link Dialog}. * * @return Emoticons {@link Dialog} */ private Dialog createEmoticonsDialog() { final Dialog d = new Dialog(this); d.setTitle(R.string.emo_); d.setContentView(R.layout.emo); d.setCancelable(true); final String[] emoticons = this.getResources().getStringArray(R.array.emoticons); final GridView gridview = (GridView) d.findViewById(R.id.gridview); gridview.setAdapter(new BaseAdapter() { // references to our images // keep order and count synced with string-array! private Integer[] mThumbIds = { R.drawable.emo_im_angel, R.drawable.emo_im_cool, R.drawable.emo_im_crying, R.drawable.emo_im_foot_in_mouth, R.drawable.emo_im_happy, R.drawable.emo_im_kissing, R.drawable.emo_im_laughing, R.drawable.emo_im_lips_are_sealed, R.drawable.emo_im_money_mouth, R.drawable.emo_im_sad, R.drawable.emo_im_surprised, R.drawable.emo_im_tongue_sticking_out, R.drawable.emo_im_undecided, R.drawable.emo_im_winking, R.drawable.emo_im_wtf, R.drawable.emo_im_yelling }; @Override public long getItemId(final int position) { return 0; } @Override public Object getItem(final int position) { return null; } @Override public int getCount() { return this.mThumbIds.length; } @Override public View getView(final int position, final View convertView, final ViewGroup parent) { ImageView imageView; if (convertView == null) { // if it's not recycled, // initialize some attributes imageView = new ImageView(WebSMS.this); imageView.setLayoutParams(new GridView.LayoutParams(EMOTICONS_SIZE, EMOTICONS_SIZE)); imageView.setScaleType(ImageView.ScaleType.CENTER_CROP); imageView.setPadding(EMOTICONS_PADDING, EMOTICONS_PADDING, EMOTICONS_PADDING, EMOTICONS_PADDING); } else { imageView = (ImageView) convertView; } imageView.setImageResource(this.mThumbIds[position]); return imageView; } }); gridview.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(final AdapterView<?> adapter, final View v, final int id, final long arg3) { EditText et = WebSMS.this.etText; final String e = emoticons[id]; int i = et.getSelectionStart(); int j = et.getSelectionEnd(); if (i > j) { int x = i; i = j; j = x; } String t = et.getText().toString(); et.setText(t.substring(0, i) + e + t.substring(j)); et.setSelection(i + e.length()); d.dismiss(); et.requestFocus(); } }); return d; } @Override protected final Dialog onCreateDialog(final int id) { AlertDialog.Builder builder; switch (id) { case DIALOG_CUSTOMSENDER: builder = new AlertDialog.Builder(this); builder.setTitle(R.string.custom_sender); builder.setCancelable(true); final EditText et = new EditText(this); builder.setView(et); builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(final DialogInterface dialog, final int id) { WebSMS.lastCustomSender = et.getText().toString(); } }); builder.setNegativeButton(android.R.string.cancel, null); return builder.create(); case DIALOG_SENDLATER_DATE: Calendar c = Calendar.getInstance(); return new DatePickerDialog(this, this, c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DAY_OF_MONTH)); case DIALOG_SENDLATER_TIME: c = Calendar.getInstance(); return new MyTimePickerDialog(this, this, c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE), true); case DIALOG_EMO: return this.createEmoticonsDialog(); default: return null; } } /** * Log text. * * @param text text as resID */ public final void log(final int text) { this.log(this.getString(text)); } /** * Log text. * * @param text text */ public final void log(final String text) { try { Toast.makeText(this.getApplicationContext(), text, Toast.LENGTH_LONG).show(); } catch (RuntimeException e) { Log.e(TAG, null, e); } } /** * Safe draft. */ private void saveDraft() { // fetch text/recipient final String to = this.etTo.getText().toString(); final String text = this.etText.getText().toString(); if (to.length() == 0 || text.length() == 0) { return; } final String[] tos = Utils.parseRecipients(to); final ConnectorCommand command = ConnectorCommand.send(nextMsgId(this), null, null, null, tos, text, false); WebSMSReceiver.saveMessage(this, null, command, WebSMSReceiver.MESSAGE_TYPE_DRAFT); this.reset(false); } /** * Send text. * * @param connector which connector should be used. * @param subconnector which subconnector should be used * @return true if message was sent */ private boolean send(final ConnectorSpec connector, final SubConnectorSpec subconnector) { Log.d(TAG, "send(" + connector + "," + subconnector + ")"); if (connector == null || subconnector == null) { Log.e(TAG, "connector: " + connector); Log.e(TAG, "subconnector: " + subconnector); Toast.makeText(this, R.string.error, Toast.LENGTH_LONG).show(); return false; } if (BuildConfig.APPLICATION_ID.equals(connector.getPackage()) && !requestPermission(Manifest.permission.SEND_SMS, PERMISSIONS_REQUEST_SEND_SMS, R.string.permissions_send_sms, null)) { return false; } // fetch text/recipient final String to = this.etTo.getText().toString(); String text = this.etText.getText().toString(); if (TextUtils.isEmpty(to) || TextUtils.isEmpty(text)) { Log.e(TAG, "to: " + to); Log.e(TAG, "text: " + text); return false; } text = text.trim(); this.etText.setText(text); final SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(this); final String signature = p.getString(PREFS_SIGNATURE, null); if (signature != null && signature.length() > 0 && !text.endsWith(signature)) { text = text + signature; this.etText.setText(text); } final String[] tos = Utils.parseRecipients(to); if (tos.length == 0) { Log.e(TAG, "tos list empty"); return false; } if (connector.getPackage().equals(PseudoConnectorRules.ID)) { return sendByRules(tos, text); } else { return sendReal(connector, subconnector, tos, text); } } /** * Send text to connector based on rules. * * @param tos recipients * @param text message text * @return true if message was sent */ private boolean sendByRules(final String[] tos, final String text) { // find connector for each of the recipients, // group together recipients that will use the same connector Map<ConnectorLabel, List<String>> chosenMap = new HashMap<>(); try { for (String to : tos) { ConnectorLabel chosenConn = rules.chooseConnector(this, to, text); List<String> tosForChosen = chosenMap.get(chosenConn); if (tosForChosen == null) { tosForChosen = new ArrayList<>(); chosenMap.put(chosenConn, tosForChosen); } tosForChosen.add(to); } } catch (WebSMSException e) { Toast.makeText(this, e.getLocalizedMessage(), Toast.LENGTH_LONG).show(); return false; } // dispatch for (Map.Entry<ConnectorLabel, List<String>> entry : chosenMap.entrySet()) { ConnectorLabel chosenConn = entry.getKey(); String[] tosForChosen = entry.getValue().toArray(new String[entry.getValue().size()]); if (PseudoConnectorRules.isTestOnly(this)) { Toast.makeText(this, getString(R.string.rules_test_only, Utils.joinRecipients(tosForChosen, ","), chosenConn.getName()), Toast.LENGTH_LONG).show(); } else { if (PseudoConnectorRules.isShowDecisionToast(this)) { Toast.makeText(this, getString(R.string.rules_decision, Utils.joinRecipients(tosForChosen, ","), chosenConn.getName()), Toast.LENGTH_LONG).show(); } boolean ok = sendReal(chosenConn.getConnector(), chosenConn.getSubConnector(), tosForChosen, text); if (!ok) { return false; } } } return true; } /** * Send text after the real connector is chosen. * * @param connector which connector should be used. * @param subconnector which subconnector should be used * @param tos recipients * @param text message text * @return true if message was sent */ private boolean sendReal(final ConnectorSpec connector, final SubConnectorSpec subconnector, final String[] tos, final String text) { Log.d(TAG, "sendReal(" + connector + "," + subconnector + ")"); final SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(this); if (!p.getBoolean(PREFS_TRY_SEND_INVALID, false) && connector.hasCapabilities(ConnectorSpec.CAPABILITIES_CHARACTER_CHECK)) { final String valid = connector.getValidCharacters(); if (valid == null) { Log.i(TAG, "valid: null"); Toast.makeText(this, R.string.log_error_char_nonvalid, Toast.LENGTH_LONG).show(); return false; } final Pattern checkPattern = Pattern.compile("[^" + Pattern.quote(valid) + "]+"); Log.d(TAG, "pattern: " + checkPattern.pattern()); final Matcher m = checkPattern.matcher(text); if (m.find()) { final String illigal = m.group(); Log.i(TAG, "invalid character: " + illigal); Toast.makeText(this, this.getString(R.string.log_error_char_notsendable) + ": " + illigal, Toast.LENGTH_LONG).show(); return false; } } ToggleButton v = (ToggleButton) this.findViewById(R.id.flashsms); final boolean flashSMS = (v.getVisibility() == View.VISIBLE) && v.isEnabled() && v.isChecked(); final String defPrefix = p.getString(PREFS_DEFPREFIX, "+49"); final String defSender = p.getString(PREFS_SENDER, ""); final ConnectorCommand command = ConnectorCommand.send(nextMsgId(this), subconnector.getID(), defPrefix, defSender, tos, text, flashSMS); command.setCustomSender(lastCustomSender); command.setSendLater(lastSendLater); boolean sent = false; try { if (subconnector.hasFeatures(SubConnectorSpec.FEATURE_MULTIRECIPIENTS) || tos.length == 1) { Log.d(TAG, "text: " + text); Log.d(TAG, "to: ", tos); runCommand(this, connector, command); } else { ConnectorCommand cc; for (String t : tos) { if (t.trim().length() < 1) { continue; } cc = (ConnectorCommand) command.clone(); cc.setRecipients(t); Log.d(TAG, "text: " + text); Log.d(TAG, "to: ", tos); runCommand(this, connector, cc); } } sent = true; } catch (Exception e) { Log.e(TAG, "error running command", e); Toast.makeText(this, R.string.error, Toast.LENGTH_LONG).show(); } if (sent) { this.reset(false); try { Thread.sleep(SLEEP_BEFORE_EXIT); } catch (InterruptedException e) { Log.e(TAG, null, e); } this.finish(); return true; } return false; } /** * A Date was set. * * @param view DatePicker View * @param year year set * @param monthOfYear month set * @param dayOfMonth day set */ public final void onDateSet(final DatePicker view, final int year, final int monthOfYear, final int dayOfMonth) { final Calendar c = Calendar.getInstance(); if (lastSendLater > 0) { c.setTimeInMillis(lastSendLater); } c.set(Calendar.YEAR, year); c.set(Calendar.MONTH, monthOfYear); c.set(Calendar.DAY_OF_MONTH, dayOfMonth); lastSendLater = c.getTimeInMillis(); MyTimePickerDialog .setOnlyQuaters(prefsSubConnectorSpec.hasFeatures(SubConnectorSpec.FEATURE_SENDLATER_QUARTERS)); this.showDialog(DIALOG_SENDLATER_TIME); this.setButtons(); } /** * A Time was set. * * @param view TimePicker View * @param hour hour set * @param minutes minutes set */ public final void onTimeSet(final TimePicker view, final int hour, final int minutes) { if (prefsSubConnectorSpec.hasFeatures(SubConnectorSpec.FEATURE_SENDLATER_QUARTERS) && minutes % 15 != 0) { Toast.makeText(this, R.string.error_sendlater_quater, Toast.LENGTH_LONG).show(); return; } final Calendar c = Calendar.getInstance(); if (lastSendLater > 0) { c.setTimeInMillis(lastSendLater); } c.set(Calendar.HOUR_OF_DAY, hour); c.set(Calendar.MINUTE, minutes); lastSendLater = c.getTimeInMillis(); this.setButtons(); } /** * Add or update a {@link ConnectorSpec}. * * @param connector connector */ static void addConnector(final ConnectorSpec connector, final ConnectorCommand command) { synchronized (CONNECTORS) { if (connector == null || connector.getPackage() == null || connector.getName() == null) { return; } ConnectorSpec c = getConnectorByID(connector.getPackage()); if (c != null) { Log.d(TAG, "update connector with id: " + c.getPackage()); Log.d(TAG, "update connector with name: " + c.getName()); c.setErrorMessage((String) null); // fix sticky error status short wasRunningStatus = c.getRunningStatus(); c.update(connector); if (command.getType() == ConnectorCommand.TYPE_NONE && wasRunningStatus != 0 && c.hasStatus(ConnectorSpec.STATUS_ENABLED)) { // if this info is not a response to a command then preserve the running status // unless we've learnt that this connector got disabled Log.d(TAG, "preserving running status"); c.addStatus(wasRunningStatus); } if (me != null) { final SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(me); final String em = c.getErrorMessage(); if (em != null) { if (command.getType() != ConnectorCommand.TYPE_SEND) { Toast.makeText(me, em, Toast.LENGTH_LONG).show(); } } else if (p.getBoolean(PREFS_SHOW_BALANCE_TOAST, false) && !TextUtils.isEmpty(c.getBalance())) { Toast.makeText(me, c.getName() + ": " + c.getBalance(), Toast.LENGTH_LONG).show(); } } } else { --newConnectorsExpected; final String pkg = connector.getPackage(); final String name = connector.getName(); if (connector.getSubConnectorCount() == 0 || name == null || pkg == null) { Log.w(TAG, "skipped adding defect connector: " + pkg); return; } Log.d(TAG, "add connector with id: " + pkg); Log.d(TAG, "add connector with name: " + name); boolean added = false; final int l = CONNECTORS.size(); ConnectorSpec cs; try { for (int i = 0; i < l; i++) { cs = CONNECTORS.get(i); if (name.compareToIgnoreCase(cs.getName()) < 0) { CONNECTORS.add(i, connector); added = true; break; } } } catch (NullPointerException e) { Log.e(TAG, "error while sorting", e); } if (!added) { CONNECTORS.add(connector); } c = connector; if (me != null) { final SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(me); // update connectors balance if needed if (c.getBalance() == null && c.isReady() && !c.isRunning() && c.hasCapabilities(ConnectorSpec.CAPABILITIES_UPDATE) && p.getBoolean(PREFS_AUTOUPDATE, true)) { final String defPrefix = p.getString(PREFS_DEFPREFIX, "+49"); final String defSender = p.getString(PREFS_SENDER, ""); runCommand(me, c, ConnectorCommand.update(defPrefix, defSender)); } } } if (me != null) { if (prefsConnectorSpec == null) { final SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(me); final String prefsConnectorID = p.getString(PREFS_CONNECTOR_ID, ""); if (prefsConnectorID.equals(connector.getPackage())) { prefsConnectorSpec = connector; final String prefsSubConnectorID = p.getString(PREFS_SUBCONNECTOR_ID, ""); prefsSubConnectorSpec = connector.getSubConnector(prefsSubConnectorID); if (prefsSubConnectorSpec == null) { prefsSubConnectorSpec = connector.getSubConnectors()[0]; } me.setButtons(); } } final String b = c.getBalance(); final String ob = c.getOldBalance(); if (b != null && (ob == null || !b.equals(ob))) { me.updateBalance(); } updateProgressBar(); if (prefsConnectorSpec != null && prefsConnectorSpec.equals(c)) { me.setButtons(); } } me.invalidateOptionsMenu(); } } /** * Get {@link ConnectorSpec} by ID. * * @param id ID * @return {@link ConnectorSpec} */ private static ConnectorSpec getConnectorByID(final String id) { if (id == null) { return null; } ConnectorSpec c; synchronized (CONNECTORS) { final int l = CONNECTORS.size(); for (int i = 0; i < l; i++) { c = CONNECTORS.get(i); if (id.equals(c.getPackage())) { return c; } } } synchronized (PSEUDO_CONNECTORS) { final int l = PSEUDO_CONNECTORS.size(); for (int i = 0; i < l; i++) { c = PSEUDO_CONNECTORS.get(i); if (id.equals(c.getPackage())) { return c; } } } return null; } /** * Get {@link ConnectorSpec}s by capabilities and/or status. * * @param capabilities capabilities needed * @param status status required {@link SubConnectorSpec} * @param isIncludePseudoConnectors whether pseudo connectors should be included * @return {@link ConnectorSpec}s */ public static ConnectorSpec[] getConnectors(final int capabilities, final int status, final boolean isIncludePseudoConnectors) { final ArrayList<ConnectorSpec> ret = new ArrayList<ConnectorSpec>( CONNECTORS.size() + PSEUDO_CONNECTORS.size()); ConnectorSpec c; synchronized (CONNECTORS) { final int l = CONNECTORS.size(); for (int i = 0; i < l; i++) { c = CONNECTORS.get(i); if (c.hasCapabilities((short) capabilities) && c.hasStatus((short) status)) { ret.add(c); } } } if (isIncludePseudoConnectors && ret.size() > 0) { // note: pseudo-connectors are only included if real connectors are also available // because pseudo-connectors do not make sense without them synchronized (PSEUDO_CONNECTORS) { final int l = PSEUDO_CONNECTORS.size(); for (int i = 0; i < l; i++) { c = PSEUDO_CONNECTORS.get(i); if (c.hasCapabilities((short) capabilities) && c.hasStatus((short) status)) { ret.add(c); } } } } return ret.toArray(new ConnectorSpec[ret.size()]); } /** * Get the number of connector applications that are installed on the * system. * * @return the number of connector applications */ private int getInstalledConnectorsCount() { final List<ResolveInfo> ri = this.getPackageManager() .queryBroadcastReceivers(new Intent(Connector.ACTION_CONNECTOR_UPDATE), 0); return ri.size(); } /** * Enables or disables indeterminate progress bar based on the current state. */ private static void updateProgressBar() { if (me != null) { boolean needProgressBar; if (newConnectorsExpected > 0) { Log.d(TAG, "expecting connector info: " + newConnectorsExpected); needProgressBar = true; } else { ConnectorSpec[] running = getConnectors(ConnectorSpec.CAPABILITIES_UPDATE, ConnectorSpec.STATUS_ENABLED | ConnectorSpec.STATUS_UPDATING, false /*isIncludePseudoConnectors*/); Log.d(TAG, "running connectors: " + running.length); if (running.length != 0) { needProgressBar = true; } else { ConnectorSpec[] booting = getConnectors(ConnectorSpec.CAPABILITIES_BOOTSTRAP, ConnectorSpec.STATUS_ENABLED | ConnectorSpec.STATUS_BOOTSTRAPPING, false /*isIncludePseudoConnectors*/); Log.d(TAG, "booting connectors: " + booting.length); needProgressBar = (booting.length != 0); } } me.setSupportProgressBarIndeterminateVisibility(needProgressBar); } } /** * Generates unique id for the next message. * * @param context Current context * @return message id */ private static synchronized long nextMsgId(final Context context) { final SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(context); long nextMsgId = p.getLong(PREFS_LAST_MSG_ID, 0) + 1; SharedPreferences.Editor editor = p.edit(); editor.putLong(PREFS_LAST_MSG_ID, nextMsgId); editor.apply(); return nextMsgId; } }