Initial import
[SaveMySugar/android-savemysugar.git] / src / main / java / it / ao2 / savemysugar / fragments / SendFragment.java
1 /*
2  * SaveMySugar - Exchange messages using the distance between phone calls
3  *
4  * Copyright (C) 2015  Antonio Ospite <ao2@ao2.it>
5  *
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.
10  *
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.
15  *
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/>.
18  */
19
20 package it.ao2.savemysugar;
21
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;
41
42 import java.lang.reflect.Method;
43 import java.util.List;
44 import java.util.Locale;
45
46 import it.ao2.savemysugar.util.TimeLog;
47 import it.ao2.util.morse.MorseDistanceModulator;
48 import it.ao2.util.morse.MorseTranslator;
49
50 public class SendFragment extends Fragment {
51
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;
59
60     public static class EndCallReceiver extends BroadcastReceiver {
61
62         @Override
63         public void onReceive(Context context, Intent intent) {
64             endCall(context);
65         }
66
67         public void endCall(Context context) {
68             TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
69             try {
70                 Class<?> c = Class.forName(tm.getClass().getName());
71                 Method m = c.getDeclaredMethod("getITelephony");
72                 m.setAccessible(true);
73                 Object telephonyService = m.invoke(tm);
74
75                 c = Class.forName(telephonyService.getClass().getName());
76                 m = c.getDeclaredMethod("endCall");
77                 m.setAccessible(true);
78                 m.invoke(telephonyService);
79
80             } catch (Exception e) {
81                 e.printStackTrace();
82             }
83         }
84     }
85
86     @Override
87     public void onAttach(Context context) {
88         super.onAttach(context);
89         mModulator = ((SaveMySugarApplication) getActivity().getApplication()).getModulator();
90     }
91
92     @Override
93     public View onCreateView(LayoutInflater inflater, ViewGroup container,
94                              Bundle savedInstanceState) {
95         View view = inflater.inflate(R.layout.fragment_send, container, false);
96
97         mLogWindow = (TextView) view.findViewById(R.id.send_log_window);
98         mLogWindow.setMovementMethod(new ScrollingMovementMethod());
99
100         final Button sendMessageButton = (Button) view.findViewById(R.id.send_message);
101         sendMessageButton.setOnClickListener(new View.OnClickListener() {
102             public void onClick(View v) {
103                 sendMessage();
104             }
105         });
106
107         return view;
108     }
109
110     @Override
111     public void onDestroy() {
112         super.onDestroy();
113
114         cancelEndCallAlarm();
115         if (mSendSymbolHandler != null) {
116             mSendSymbolHandler.removeCallbacksAndMessages(null);
117         }
118     }
119
120     private void setEndCallAlarm(long timeout) {
121         Intent intent = new Intent(getActivity(), EndCallReceiver.class);
122         PendingIntent pi = PendingIntent.getBroadcast(getActivity().getApplicationContext(), 0, intent, 0);
123
124         AlarmManager alarmManager = (AlarmManager) getActivity().getSystemService(Context.ALARM_SERVICE);
125         alarmManager.set(AlarmManager.RTC_WAKEUP, timeout, pi);
126     }
127
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);
131
132         AlarmManager alarmManager = (AlarmManager) getActivity().getSystemService(Context.ALARM_SERVICE);
133         alarmManager.cancel(pi);
134     }
135
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);
145
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");
151
152             try {
153                 Intent callIntent = new Intent(Intent.ACTION_CALL);
154                 callIntent.setData(Uri.parse("tel:" + phoneNumber));
155
156                 // Set the alarm right before starting the ACTION_CALL
157                 // activity to minimize the risk of a premature hangup.
158                 //
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
162                 // placed.
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);
168             }
169         }
170     }
171
172     @Override
173     public void onActivityResult(int requestCode, int resultCode, Intent data) {
174         super.onActivityResult(requestCode, resultCode, data);
175
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.
187             //
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.));
192
193             long distance = (long) (mDistances.get(requestCode) * 1000) - delay;
194             if (distance < 0) {
195                 distance = 0;
196             }
197
198             Log.d(TAG, "Should sleep " + mDistances.get(requestCode) + ". Will sleep " + (distance / 1000.));
199
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() {
204                 @Override
205                 public void run() {
206                     sendSymbol(mDestinationNumber, nextRequest);
207                     mSendSymbolHandler = null;
208                 }
209             }, distance);
210         }
211     }
212
213     /** Called when the user clicks the Send button */
214     public void sendMessage() {
215         Log.d(TAG, "sending Message");
216         View view = getView();
217
218         EditText editNumber = (EditText) view.findViewById(R.id.destination_number);
219         mDestinationNumber = editNumber.getText().toString().trim();
220
221         EditText editMessage = (EditText) view.findViewById(R.id.message);
222         String message = editMessage.getText().toString();
223
224         MorseTranslator translator =  ((SaveMySugarApplication) getActivity().getApplication()).getTranslator();
225         String morse = translator.textToMorse(message);
226
227         mDistances = mModulator.modulate(morse);
228
229         mLogWindow.setText(morse);
230         mLogWindow.append("\n");
231
232         if (mDistances.size() > 0) {
233             sendSymbol(mDestinationNumber, 0);
234         }
235     }
236 }