Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ render.experimental.xml
*.keystore
*.hprof
app/src/main/jniLibs/
app/bin/*
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"java.jdt.ls.androidSupport.enabled": "on"
}
4 changes: 3 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
<!--
This permission is required to open another activity from an app in the background
(i.e. AnkiConnectAndroid opening AnkiDroid when the kiwi browser is in focus)
Expand Down Expand Up @@ -55,7 +56,8 @@
android:resource="@xml/file_provider_paths" />
</provider>

<service android:name=".Service" />
<service android:name=".Service"
android:foregroundServiceType="dataSync" />
</application>

</manifest>
76 changes: 65 additions & 11 deletions app/src/main/java/com/kamwithk/ankiconnectandroid/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import android.widget.Button;
import androidx.appcompat.app.AppCompatActivity;

import androidx.activity.result.ActivityResultLauncher;
Expand All @@ -22,9 +23,10 @@
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.Observer;

import com.kamwithk.ankiconnectandroid.ankidroid_api.IntegratedAPI;

import com.kamwithk.ankiconnectandroid.ankidroid_api.IntegratedAPI;

public class MainActivity extends AppCompatActivity {

Expand Down Expand Up @@ -56,6 +58,8 @@ public void onClick(DialogInterface dialog, int id) {
public static final String CHANNEL_ID = "ankiConnectAndroid";
private NotificationManager notificationManager;
private ActivityResultLauncher<String> requestPermissionLauncher;
private Button startServiceButton;
private Button stopServiceButton;

@Override
protected void onCreate(Bundle savedInstanceState) {
Expand All @@ -64,23 +68,47 @@ protected void onCreate(Bundle savedInstanceState) {

// toolbar support
Toolbar toolbar = findViewById(R.id.materialToolbar);

try {
startServiceButton = findViewById(R.id.start_service_btn);
stopServiceButton = findViewById(R.id.stop_service_btn);
} catch (Exception e) {
android.util.Log.e("MainActivity", "Could not find start/stop buttons. Check IDs in XML.", e);
}

setSupportActionBar(toolbar);

IntegratedAPI.authenticate(this);

NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID, "Ankiconnect Android", NotificationManager.IMPORTANCE_DEFAULT);
NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID, "Ankiconnect Android",
NotificationManager.IMPORTANCE_DEFAULT);
notificationManager = getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(notificationChannel);

// this cannot be put inside attemptGrantNotifyPermissions, because it is called by
// this cannot be put inside attemptGrantNotifyPermissions, because it is called
// by
// a onClickListener and crashes the app: https://stackoverflow.com/a/67582633
requestPermissionLauncher =
registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(),
isGranted -> {
if (!isGranted) {
Toast.makeText(this, "Attempting to start server without notification...", Toast.LENGTH_LONG).show();
Toast.makeText(this, "Attempting to start server without notification...", Toast.LENGTH_LONG)
.show();
}
startService();
});

// Observe the service running state
Service.serviceRunningState.observe(this, new Observer<Boolean>() {
@Override
public void onChanged(Boolean isRunning) {
updateButtonStates(isRunning);
}
});
}

protected void onStart() {
super.onStart();
startServiceWrap();
}

@Override
Expand Down Expand Up @@ -117,13 +145,17 @@ public void attemptGrantNotificationPermissions() {
// explanation for shouldShowRequestPermissionRationale is shown below
// (taken from: https://stackoverflow.com/a/39739972):
//
// This method returns true if the app has requested this permission previously and the
// user denied the request. Note: If the user turned down the permission request in the
// past and chose the Don't ask again option in the permission request system dialog,
// This method returns true if the app has requested this permission previously
// and the
// user denied the request. Note: If the user turned down the permission request
// in the
// past and chose the Don't ask again option in the permission request system
// dialog,
// this method returns false.
if (shouldShowRequestPermissionRationale(POST_NOTIFICATIONS)) {
// Explain that notifications are "needed" to display the server
new NotificationsPermissionDialogFragment().show(this.getSupportFragmentManager(), "post_notifications_dialog");
new NotificationsPermissionDialogFragment().show(this.getSupportFragmentManager(),
"post_notifications_dialog");
} else {
// Directly ask for the permission.
requestPermissionLauncher.launch(POST_NOTIFICATIONS);
Expand All @@ -141,8 +173,11 @@ public void startService() {
ContextCompat.startForegroundService(this, serviceIntent);
}


public void startServiceBtn(View view) {
startServiceWrap();
}

public void startServiceWrap() {
boolean notificationsEnabled = notificationManager.areNotificationsEnabled();
if (notificationsEnabled) {
startService();
Expand All @@ -155,4 +190,23 @@ public void stopServiceBtn(View view) {
Intent serviceIntent = new Intent(this, Service.class);
stopService(serviceIntent);
}

private void updateButtonStates(boolean isRunning) {
if (startServiceButton == null || stopServiceButton == null) {
return;
}

if (isRunning) {
startServiceButton.setEnabled(false);
stopServiceButton.setEnabled(true);
startServiceButton.setText("Server is running at port " + Service.PORT);
stopServiceButton.setText("Stop Service");
return;
}

startServiceButton.setEnabled(true);
stopServiceButton.setEnabled(false);
startServiceButton.setText("Start Service");
stopServiceButton.setText("Server stopped");
}
}
20 changes: 16 additions & 4 deletions app/src/main/java/com/kamwithk/ankiconnectandroid/Service.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.lifecycle.MutableLiveData;
import com.kamwithk.ankiconnectandroid.routing.Router;

import java.io.IOException;
Expand All @@ -19,15 +20,19 @@ public class Service extends android.app.Service {

private Router server;

// LiveData to observe service state
public static final MutableLiveData<Boolean> serviceRunningState = new MutableLiveData<>(false);

@Override
public void onCreate() { // Only one time
super.onCreate();

try {
server = new Router(PORT, this);
} catch (IOException e) {
Log.w("Httpd", "The Server was unable to start");
e.printStackTrace();
Log.e("Httpd", "The Server was unable to start.", e);
stopSelf(); // Stop the service if the server can't start
return;
}
}

Expand All @@ -44,22 +49,29 @@ public int onStartCommand(Intent intent, int flags, int startId) { // Every time

Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Ankiconnect Android")
.setContentText("AnkiConnect service is running.")
.setSmallIcon(R.mipmap.app_launcher)
.setContentIntent(pendingIntent)
.setOngoing(true)
.build();

startForeground(1, notification);

// Update LiveData on main thread
serviceRunningState.postValue(true);
return START_STICKY;
}

@Override
public void onDestroy() {
server.stop();
if (server != null) {
server.stop();
}

// Update LiveData on main thread
serviceRunningState.postValue(false);
super.onDestroy();
}

@Nullable
@Override
public IBinder onBind(Intent intent) {
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,14 @@
app:layout_constraintTop_toTopOf="parent" />

<Button
android:id="@+id/start_service_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="startServiceBtn"
android:text="Start Service" />

<Button
android:id="@+id/stop_service_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="stopServiceBtn"
Expand Down