Start background service scaffolding

This commit is contained in:
Garrett Mills 2021-05-15 12:52:25 -05:00
parent 2e479350de
commit a4017c303e
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246
11 changed files with 363 additions and 4 deletions

View File

@ -0,0 +1,36 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="JavaDoc" enabled="true" level="WARNING" enabled_by_default="true">
<option name="TOP_LEVEL_CLASS_OPTIONS">
<value>
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
<option name="REQUIRED_TAGS" value="" />
</value>
</option>
<option name="INNER_CLASS_OPTIONS">
<value>
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
<option name="REQUIRED_TAGS" value="" />
</value>
</option>
<option name="METHOD_OPTIONS">
<value>
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
<option name="REQUIRED_TAGS" value="@return@param@throws or @exception" />
</value>
</option>
<option name="FIELD_OPTIONS">
<value>
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
<option name="REQUIRED_TAGS" value="" />
</value>
</option>
<option name="IGNORE_DEPRECATED" value="false" />
<option name="IGNORE_JAVADOC_PERIOD" value="true" />
<option name="IGNORE_DUPLICATED_THROWS" value="false" />
<option name="IGNORE_POINT_TO_ITSELF" value="false" />
<option name="myAdditionalJavadocTags" value="fixme" />
</inspection_tool>
</profile>
</component>

View File

@ -6,6 +6,10 @@
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_SMS" />
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application <application
android:allowBackup="true" android:allowBackup="true"
@ -14,7 +18,12 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.StarshipHyperlink"> android:theme="@style/Theme.StarshipHyperlink">
<activity android:name=".LoginTokenFormActivity"></activity> <service
android:name=".MessagingService"
android:enabled="true"
android:exported="true"></service>
<activity android:name=".LoginTokenFormActivity" />
<activity android:name=".LoginTokenScannerActivity" /> <activity android:name=".LoginTokenScannerActivity" />
<activity android:name=".MainActivity"> <activity android:name=".MainActivity">
<intent-filter> <intent-filter>

View File

@ -5,6 +5,7 @@ import android.content.SharedPreferences;
import com.android.volley.RequestQueue; import com.android.volley.RequestQueue;
public class Hyperlink { public class Hyperlink {
public static final int MESSAGE_TICK_POLL_INTERVAL_MS = 5000;
public static final String SHARED_PREFERENCES_NAME = "dev.garrettmills.starship.hyperlink.main"; public static final String SHARED_PREFERENCES_NAME = "dev.garrettmills.starship.hyperlink.main";
public static final String SERVER_ADDR = "dev.garrettmills.starship.hyperlink.server"; public static final String SERVER_ADDR = "dev.garrettmills.starship.hyperlink.server";
public static final String ACCESS_TOKEN = "dev.garrettmills.starship.hyperlink.token.server"; public static final String ACCESS_TOKEN = "dev.garrettmills.starship.hyperlink.token.server";
@ -13,6 +14,10 @@ public class Hyperlink {
public static final int REQUEST_LOGIN_TOKEN = 180; public static final int REQUEST_LOGIN_TOKEN = 180;
public static final int REQUEST_PERMISSION_CAMERA = 181; public static final int REQUEST_PERMISSION_CAMERA = 181;
public static final int REQUEST_PERMISSION_READ_SMS = 182;
public static final String NOTIFICATION_CHANNEL = "dev.garrettmills.starship.hyperlink.notifications";
public static final int SERVICE_NOTIFICATION_ID = 183;
public static SharedPreferences preferences; public static SharedPreferences preferences;
public static RequestQueue httpRequestQueue; public static RequestQueue httpRequestQueue;

View File

@ -1,22 +1,49 @@
package dev.garrettmills.starship.hyperlink; package dev.garrettmills.starship.hyperlink;
import androidx.appcompat.app.AppCompatActivity; import android.Manifest;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.Bundle; import android.os.Bundle;
import android.provider.Telephony;
import android.util.Log;
import android.view.View; import android.view.View;
import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import com.android.volley.toolbox.Volley; import com.android.volley.toolbox.Volley;
import dev.garrettmills.starship.hyperlink.util.APIv1;
import dev.garrettmills.starship.hyperlink.util.AccessToken;
import dev.garrettmills.starship.hyperlink.util.InvalidLoginTokenException;
import dev.garrettmills.starship.hyperlink.util.LoginToken;
import dev.garrettmills.starship.hyperlink.util.NotAuthenticatedException;
public class MainActivity extends AppCompatActivity { public class MainActivity extends AppCompatActivity {
TextView serverAddressView;
Intent serviceIntent = null;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
Hyperlink.preferences = getSharedPreferences(Hyperlink.SHARED_PREFERENCES_NAME, MODE_PRIVATE); Hyperlink.preferences = getSharedPreferences(Hyperlink.SHARED_PREFERENCES_NAME, MODE_PRIVATE);
Hyperlink.httpRequestQueue = Volley.newRequestQueue(this); Hyperlink.httpRequestQueue = Volley.newRequestQueue(this);
setContentView(R.layout.activity_main); NotificationChannel channel = new NotificationChannel(Hyperlink.NOTIFICATION_CHANNEL, "Starship Hyperlink", NotificationManager.IMPORTANCE_LOW);
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
assert manager != null;
manager.createNotificationChannel(channel);
if (APIv1.isAuthenticated()) switchToStatusMode();
else switchToLoginMode();
} }
public void onScanTokenClick(View view) { public void onScanTokenClick(View view) {
@ -29,6 +56,76 @@ public class MainActivity extends AppCompatActivity {
startActivityForResult(intent, Hyperlink.REQUEST_LOGIN_TOKEN); startActivityForResult(intent, Hyperlink.REQUEST_LOGIN_TOKEN);
} }
public void onLogoutButtonClick(View view) {
switchToLoginMode();
}
protected void switchToStatusMode() {
setContentView(R.layout.activity_main_status);
serverAddressView = findViewById(R.id.activity_main_serverAddressTextView);
try {
AccessToken token = APIv1.getToken();
serverAddressView.setText(token.getServer());
} catch (NotAuthenticatedException e) {
if ( BuildConfig.DEBUG ) {
Toast.makeText(getApplicationContext(), "Cannot fetch token; not authenticated.", Toast.LENGTH_SHORT).show();
}
APIv1.logout();
switchToLoginMode();
return;
}
requestSMSRead();
Cursor cursor = getContentResolver().query(Telephony.Threads.CONTENT_URI, null, null, null, "date DESC");
if ( cursor.moveToFirst() ) {
int j = 0;
do {
j += 1;
if ( j > 5 ) break;
for ( int i = 0; i < cursor.getColumnCount(); i += 1 ) {
Log.i("MainActivity", cursor.getColumnName(i) + ": " + cursor.getString(i));
}
} while ( cursor.moveToNext() );
}
serviceIntent = new Intent(this, MessagingService.class);
ContextCompat.startForegroundService(this, serviceIntent);
}
protected void switchToLoginMode() {
APIv1.logout();
if ( serviceIntent != null ) stopService(serviceIntent);
setContentView(R.layout.activity_main);
serverAddressView = null;
}
protected void requestSMSRead() {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_SMS) == PackageManager.PERMISSION_GRANTED) {
// switchToStatusMode();
} else {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_SMS}, Hyperlink.REQUEST_PERMISSION_READ_SMS);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == Hyperlink.REQUEST_PERMISSION_READ_SMS) {
if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
switchToStatusMode();
} else {
if ( BuildConfig.DEBUG ) {
Toast.makeText(this, "Read SMS permission denied", Toast.LENGTH_SHORT).show();
}
switchToLoginMode();
}
}
}
@Override @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
@ -38,6 +135,15 @@ public class MainActivity extends AppCompatActivity {
if ( BuildConfig.DEBUG ) { if ( BuildConfig.DEBUG ) {
Toast.makeText(getApplicationContext(), data.getExtras().getString(Hyperlink.EXTRA_LOGIN_TOKEN), Toast.LENGTH_SHORT).show(); Toast.makeText(getApplicationContext(), data.getExtras().getString(Hyperlink.EXTRA_LOGIN_TOKEN), Toast.LENGTH_SHORT).show();
} }
try {
LoginToken login = new LoginToken(data.getExtras().getString(Hyperlink.EXTRA_LOGIN_TOKEN));
APIv1.login(login);
switchToStatusMode();
} catch (InvalidLoginTokenException e) {
Toast.makeText(getApplicationContext(), "Invalid login token!", Toast.LENGTH_SHORT).show();
switchToLoginMode();
}
} }
} }
} }

View File

@ -0,0 +1,76 @@
package dev.garrettmills.starship.hyperlink;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.widget.Toast;
import java.sql.Time;
import java.util.Timer;
import java.util.TimerTask;
import dev.garrettmills.starship.hyperlink.relay.ServerSentRequest;
public class MessagingService extends Service {
private final Handler handler = new Handler();
private final Timer timer = new Timer();
public MessagingService() {
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Intent notificationIntent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
Notification notification = new Notification.Builder(this, Hyperlink.NOTIFICATION_CHANNEL)
.setContentTitle("Starship Hyperlink is Running")
.setContentText("Your text messages are being relayed to Hyperlink.")
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentIntent(pendingIntent)
.setTicker("ticker")
.build();
startForeground(Hyperlink.SERVICE_NOTIFICATION_ID, notification);
startConnection();
return START_STICKY;
}
@Override
public void onDestroy() {
timer.cancel();
}
private void startConnection() {
TimerTask task = new TimerTask() {
@Override
public void run() {
handler.post(() -> {
if ( !tick() ) {
stopSelf();
}
});
}
};
timer.schedule(task, 0, Hyperlink.MESSAGE_TICK_POLL_INTERVAL_MS);
}
private boolean tick() {
return true;
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
protected void handleServerSentRequest(ServerSentRequest request) {
}
}

View File

@ -0,0 +1,5 @@
package dev.garrettmills.starship.hyperlink.relay;
public enum ServerRequestEndpoint {
LIST_THREADS
}

View File

@ -0,0 +1,14 @@
package dev.garrettmills.starship.hyperlink.relay;
public class ServerSentRequest {
private String _uuid;
private ServerRequestEndpoint _endpoint;
public String getUUID() {
return _uuid;
}
public ServerRequestEndpoint getEndpoint() {
return _endpoint;
}
}

View File

@ -1,5 +1,7 @@
package dev.garrettmills.starship.hyperlink.util; package dev.garrettmills.starship.hyperlink.util;
import android.content.SharedPreferences;
import dev.garrettmills.starship.hyperlink.Hyperlink; import dev.garrettmills.starship.hyperlink.Hyperlink;
public class APIv1 { public class APIv1 {
@ -22,4 +24,45 @@ public class APIv1 {
return server + "/api/v1" + endpoint; return server + "/api/v1" + endpoint;
} }
public static boolean isAuthenticated() {
return !Hyperlink.preferences.getString(Hyperlink.SERVER_ADDR, "").equals("")
&& !Hyperlink.preferences.getString(Hyperlink.ACCESS_TOKEN, "").equals("");
}
public static AccessToken getToken() throws NotAuthenticatedException {
if ( !isAuthenticated() ) {
throw new NotAuthenticatedException();
}
return new AccessToken(
Hyperlink.preferences.getString(Hyperlink.SERVER_ADDR, ""),
Hyperlink.preferences.getString(Hyperlink.ACCESS_TOKEN, "")
);
}
public static void logout() {
SharedPreferences.Editor editor = Hyperlink.preferences.edit();
editor.putString(Hyperlink.SERVER_ADDR, "");
editor.putString(Hyperlink.ACCESS_TOKEN, "");
editor.apply();
}
public static AccessToken login(LoginToken login) {
AccessToken token = redeemLoginToken(login);
SharedPreferences.Editor editor = Hyperlink.preferences.edit();
editor.putString(Hyperlink.SERVER_ADDR, token.getServer());
editor.putString(Hyperlink.ACCESS_TOKEN, token.getToken());
editor.apply();
return token;
}
/**
* @fixme this is a stub placeholder. Replace with actual implementation
* @param login the LoginToken from the user
* @return the AccessToken redeemed from the server
*/
protected static AccessToken redeemLoginToken(LoginToken login) {
return new AccessToken(login.getServer(), login.getToken());
}
} }

View File

@ -1,4 +1,21 @@
package dev.garrettmills.starship.hyperlink.util; package dev.garrettmills.starship.hyperlink.util;
import androidx.annotation.NonNull;
public class AccessToken { public class AccessToken {
private final String _server;
private final String _token;
public AccessToken(@NonNull String server, @NonNull String token) {
_server = server;
_token = token;
}
public String getServer() {
return _server;
}
public String getToken() {
return _token;
}
} }

View File

@ -0,0 +1,4 @@
package dev.garrettmills.starship.hyperlink.util;
public class NotAuthenticatedException extends Exception {
}

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".MainActivity">
<Button
android:id="@+id/activity_main_logoutButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="onLogoutButtonClick"
android:text="Log Out"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.606" />
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="140dp"
android:text="Connected to Hyperlink"
android:textColor="#4CAF50"
android:textSize="20sp"
app:layout_constraintBottom_toTopOf="@+id/activity_main_logoutButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/activity_main_serverAddressTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
app:layout_constraintBottom_toTopOf="@+id/activity_main_logoutButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView2" />
</androidx.constraintlayout.widget.ConstraintLayout>