Start background service scaffolding
This commit is contained in:
		
							parent
							
								
									2e479350de
								
							
						
					
					
						commit
						a4017c303e
					
				
							
								
								
									
										36
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
									
									
									
									
										Normal 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> | ||||
| @ -6,6 +6,10 @@ | ||||
| 
 | ||||
|     <uses-permission android:name="android.permission.CAMERA" /> | ||||
|     <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 | ||||
|         android:allowBackup="true" | ||||
| @ -14,7 +18,12 @@ | ||||
|         android:roundIcon="@mipmap/ic_launcher_round" | ||||
|         android:supportsRtl="true" | ||||
|         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=".MainActivity"> | ||||
|             <intent-filter> | ||||
|  | ||||
| @ -5,6 +5,7 @@ import android.content.SharedPreferences; | ||||
| import com.android.volley.RequestQueue; | ||||
| 
 | ||||
| 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 SERVER_ADDR = "dev.garrettmills.starship.hyperlink.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_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 RequestQueue httpRequestQueue; | ||||
|  | ||||
| @ -1,22 +1,49 @@ | ||||
| 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.pm.PackageManager; | ||||
| import android.database.Cursor; | ||||
| import android.os.Bundle; | ||||
| import android.provider.Telephony; | ||||
| import android.util.Log; | ||||
| import android.view.View; | ||||
| import android.widget.TextView; | ||||
| 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 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 { | ||||
|     TextView serverAddressView; | ||||
|     Intent serviceIntent = null; | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onCreate(Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         Hyperlink.preferences = getSharedPreferences(Hyperlink.SHARED_PREFERENCES_NAME, MODE_PRIVATE); | ||||
|         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) { | ||||
| @ -29,6 +56,76 @@ public class MainActivity extends AppCompatActivity { | ||||
|         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 | ||||
|     protected void onActivityResult(int requestCode, int resultCode, Intent data) { | ||||
|         super.onActivityResult(requestCode, resultCode, data); | ||||
| @ -38,6 +135,15 @@ public class MainActivity extends AppCompatActivity { | ||||
|                 if ( BuildConfig.DEBUG ) { | ||||
|                     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(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -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) { | ||||
| 
 | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,5 @@ | ||||
| package dev.garrettmills.starship.hyperlink.relay; | ||||
| 
 | ||||
| public enum ServerRequestEndpoint { | ||||
|     LIST_THREADS | ||||
| } | ||||
| @ -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; | ||||
|     } | ||||
| } | ||||
| @ -1,5 +1,7 @@ | ||||
| package dev.garrettmills.starship.hyperlink.util; | ||||
| 
 | ||||
| import android.content.SharedPreferences; | ||||
| 
 | ||||
| import dev.garrettmills.starship.hyperlink.Hyperlink; | ||||
| 
 | ||||
| public class APIv1 { | ||||
| @ -22,4 +24,45 @@ public class APIv1 { | ||||
| 
 | ||||
|         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()); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,4 +1,21 @@ | ||||
| package dev.garrettmills.starship.hyperlink.util; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| 
 | ||||
| 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; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -0,0 +1,4 @@ | ||||
| package dev.garrettmills.starship.hyperlink.util; | ||||
| 
 | ||||
| public class NotAuthenticatedException extends Exception { | ||||
| } | ||||
							
								
								
									
										44
									
								
								app/src/main/res/layout/activity_main_status.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								app/src/main/res/layout/activity_main_status.xml
									
									
									
									
									
										Normal 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> | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user