2 * SaveMySugar - Exchange messages using the distance between phone calls
4 * Copyright (C) 2015 Antonio Ospite <ao2@ao2.it>
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
20 package it.ao2.savemysugar;
22 import android.app.AlarmManager;
23 import android.app.PendingIntent;
24 import android.content.ActivityNotFoundException;
25 import android.content.BroadcastReceiver;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.net.Uri;
29 import android.os.Bundle;
30 import android.os.Handler;
31 import android.support.v4.app.Fragment;
32 import android.telephony.TelephonyManager;
33 import android.text.method.ScrollingMovementMethod;
34 import android.util.Log;
35 import android.view.LayoutInflater;
36 import android.view.View;
37 import android.view.ViewGroup;
38 import android.widget.Button;
39 import android.widget.EditText;
40 import android.widget.TextView;
42 import java.lang.reflect.Method;
43 import java.util.List;
44 import java.util.Locale;
46 import it.ao2.savemysugar.util.TimeLog;
47 import it.ao2.util.morse.MorseDistanceModulator;
48 import it.ao2.util.morse.MorseTranslator;
50 public class SendFragment extends Fragment {
52 private static final String TAG = "SaveMySugar";
53 private TextView mLogWindow;
54 private String mDestinationNumber;
55 private MorseDistanceModulator mModulator;
56 private List<Double> mDistances;
57 private long mEndCallTime;
58 private Handler mSendSymbolHandler;
60 public static class EndCallReceiver extends BroadcastReceiver {
63 public void onReceive(Context context, Intent intent) {
67 public void endCall(Context context) {
68 TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
70 Class<?> c = Class.forName(tm.getClass().getName());
71 Method m = c.getDeclaredMethod("getITelephony");
72 m.setAccessible(true);
73 Object telephonyService = m.invoke(tm);
75 c = Class.forName(telephonyService.getClass().getName());
76 m = c.getDeclaredMethod("endCall");
77 m.setAccessible(true);
78 m.invoke(telephonyService);
80 } catch (Exception e) {
87 public void onAttach(Context context) {
88 super.onAttach(context);
89 mModulator = ((SaveMySugarApplication) getActivity().getApplication()).getModulator();
93 public View onCreateView(LayoutInflater inflater, ViewGroup container,
94 Bundle savedInstanceState) {
95 View view = inflater.inflate(R.layout.fragment_send, container, false);
97 mLogWindow = (TextView) view.findViewById(R.id.send_log_window);
98 mLogWindow.setMovementMethod(new ScrollingMovementMethod());
100 final Button sendMessageButton = (Button) view.findViewById(R.id.send_message);
101 sendMessageButton.setOnClickListener(new View.OnClickListener() {
102 public void onClick(View v) {
111 public void onDestroy() {
114 cancelEndCallAlarm();
115 if (mSendSymbolHandler != null) {
116 mSendSymbolHandler.removeCallbacksAndMessages(null);
120 private void setEndCallAlarm(long timeout) {
121 Intent intent = new Intent(getActivity(), EndCallReceiver.class);
122 PendingIntent pi = PendingIntent.getBroadcast(getActivity().getApplicationContext(), 0, intent, 0);
124 AlarmManager alarmManager = (AlarmManager) getActivity().getSystemService(Context.ALARM_SERVICE);
125 alarmManager.set(AlarmManager.RTC_WAKEUP, timeout, pi);
128 private void cancelEndCallAlarm() {
129 Intent intent = new Intent(getActivity(), EndCallReceiver.class);
130 PendingIntent pi = PendingIntent.getBroadcast(getActivity().getApplicationContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
132 AlarmManager alarmManager = (AlarmManager) getActivity().getSystemService(Context.ALARM_SERVICE);
133 alarmManager.cancel(pi);
136 // The function is called sendSymbol() for simplicity, because this is
137 // what it looks like it does to a caller, but it actually just starts
138 // the call, the symbol is represented by the distance _after_ the call
139 // ends and this is handled in onActivityResult().
140 public void sendSymbol(String phoneNumber, int index) {
141 if (!phoneNumber.equals("")) {
142 double callSetupTime = mModulator.getPeriod();
143 double distance = mDistances.get(index);
144 String symbol = (index == mDistances.size() - 1) ? "None" : mModulator.distanceToSymbol(callSetupTime + distance);
146 TimeLog.log(TAG, mLogWindow, "sendSymbol Dial and wait "
147 + String.format(Locale.US, "%.2f", (callSetupTime + distance)) + " = "
148 + String.format(Locale.US, "%.2f", callSetupTime) + " + "
149 + String.format(Locale.US, "%.2f", distance) + " seconds "
150 + "(transmitting '" + symbol + "')\n");
153 Intent callIntent = new Intent(Intent.ACTION_CALL);
154 callIntent.setData(Uri.parse("tel:" + phoneNumber));
156 // Set the alarm right before starting the ACTION_CALL
157 // activity to minimize the risk of a premature hangup.
159 // If there were interleaving operations between setting the
160 // alarm and starting the call, the call might be ended before
161 // callSetupTime has passed from when the call is actually
163 mEndCallTime = System.currentTimeMillis() + (long) (callSetupTime * 1000);
164 setEndCallAlarm(mEndCallTime);
165 startActivityForResult(callIntent, index);
166 } catch (ActivityNotFoundException e) {
167 Log.e(TAG, "Call failed", e);
173 public void onActivityResult(int requestCode, int resultCode, Intent data) {
174 super.onActivityResult(requestCode, resultCode, data);
176 // Here the requestCode value is "abused" to advance in the
177 // transmission process. The logic used here assumes that the very
178 // last distance, the one which terminates the transmission of
179 // a message, can be skipped altogether; after all there's no call
180 // after the last distance anyways, it's there just to make sure that
181 // a final call is made, and sendSymbol() takes care of that.
182 final int nextRequest = requestCode + 1;
183 if (nextRequest < mDistances.size()) {
184 // The timeout set in AlarmManager cannot be blindly trusted, it
185 // may have passed more than callSetupTime seconds since
186 // callIntent started.
188 // Account for possible delays in order to be as adherent as
189 // possible to the nominal total symbol transmission distance.
190 long delay = System.currentTimeMillis() - mEndCallTime;
191 Log.d(TAG, "Delay " + (delay / 1000.));
193 long distance = (long) (mDistances.get(requestCode) * 1000) - delay;
198 Log.d(TAG, "Should sleep " + mDistances.get(requestCode) + ". Will sleep " + (distance / 1000.));
200 // This is where the actual symbol transmission happens: in the
201 // distance before the next symbol is sent.
202 mSendSymbolHandler = new Handler();
203 mSendSymbolHandler.postDelayed(new Runnable() {
206 sendSymbol(mDestinationNumber, nextRequest);
207 mSendSymbolHandler = null;
213 /** Called when the user clicks the Send button */
214 public void sendMessage() {
215 Log.d(TAG, "sending Message");
216 View view = getView();
218 EditText editNumber = (EditText) view.findViewById(R.id.destination_number);
219 mDestinationNumber = editNumber.getText().toString().trim();
221 EditText editMessage = (EditText) view.findViewById(R.id.message);
222 String message = editMessage.getText().toString();
224 MorseTranslator translator = ((SaveMySugarApplication) getActivity().getApplication()).getTranslator();
225 String morse = translator.textToMorse(message);
227 mDistances = mModulator.modulate(morse);
229 mLogWindow.setText(morse);
230 mLogWindow.append("\n");
232 if (mDistances.size() > 0) {
233 sendSymbol(mDestinationNumber, 0);