diff --git a/.gitignore b/.gitignore
index eb5a316..141e1f9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,34 @@
-target
+# Maven
+target/
+
+# Gradle
+.gradle/
+build/
+gradle/
+gradlew
+gradlew.bat
+local.properties
+
+# IDE
+.idea/
+*.iml
+
+# Build outputs
+*.apk
+*.ap_
+*.dex
+
+# Temp files
+*.class
+*.log
+*.tmp
+*.temp
+nul
+tmpclaude-*
+
+# OS
+.DS_Store
+Thumbs.db
+
+# ADBKeyBoard (separate project)
+ADBKeyBoard/
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..635df2e
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,60 @@
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:8.1.0'
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+apply plugin: 'com.android.application'
+
+android {
+ namespace 'ca.zgrs.clipper'
+ compileSdk 34
+
+ defaultConfig {
+ applicationId "ca.zgrs.clipper"
+ minSdk 21
+ targetSdk 34
+ versionCode 3
+ versionName "2.0"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ }
+ debug {
+ minifyEnabled false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ sourceSets {
+ main {
+ manifest.srcFile 'src/main/AndroidManifest.xml'
+ java.srcDirs = ['src/main/java']
+ res.srcDirs = ['src/main/res']
+ }
+ }
+
+ lint {
+ abortOnError false
+ }
+}
+
+dependencies {
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..b2519a7
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,3 @@
+android.useAndroidX=false
+android.enableJetifier=false
+org.gradle.jvmargs=-Xmx1536m
diff --git a/pom.xml b/pom.xml
index 2f272a4..c352ce8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -9,8 +9,8 @@
clipboard-service
- 2.3.3
- 10
+ 13
+ 33
${env.ANDROID_HOME}
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..66fad80
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'Clipper'
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index a3bade4..d7c1773 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -1,33 +1,36 @@
+ android:versionCode="3" android:versionName="2.0" android:enabled="true">
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
diff --git a/src/main/java/ca/zgrs/clipper/ClipboardService.java b/src/main/java/ca/zgrs/clipper/ClipboardService.java
index b6a2f0f..db1c998 100644
--- a/src/main/java/ca/zgrs/clipper/ClipboardService.java
+++ b/src/main/java/ca/zgrs/clipper/ClipboardService.java
@@ -1,38 +1,82 @@
package ca.zgrs.clipper;
-import android.app.IntentService;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
import android.content.Intent;
-
+import android.os.Build;
+import android.os.IBinder;
import android.util.Log;
-public class ClipboardService extends IntentService {
- private static String TAG = "ClipboardService";
+public class ClipboardService extends Service {
+ private static final String TAG = "ClipboardService";
+ private static final String CHANNEL_ID = "clipper_service_channel";
+ private static final int NOTIFICATION_ID = 1;
- public ClipboardService() {
- super("ClipboardService");
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ Log.d(TAG, "ClipboardService created");
+ createNotificationChannel();
}
- /* Define service as sticky so that it stays in background */
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
- super.onStartCommand(intent, flags, startId);
+ Log.d(TAG, "ClipboardService started");
+ startForeground(NOTIFICATION_ID, createNotification());
return START_STICKY;
}
@Override
- public void onCreate() {
- super.onCreate();
- // start itself to ensure our broadcast receiver is active
- Log.d(TAG, "Start clipboard service.");
- startService(new Intent(getApplicationContext(), ClipboardService.class));
+ public IBinder onBind(Intent intent) {
+ return null;
}
- /**
- * The IntentService calls this method from the default worker thread with
- * the intent that started the service. When this method returns, IntentService
- * stops the service, as appropriate.
- */
@Override
- protected void onHandleIntent(Intent intent) {
+ public void onDestroy() {
+ Log.d(TAG, "ClipboardService destroyed");
+ super.onDestroy();
+ }
+
+ private void createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ NotificationChannel channel = new NotificationChannel(
+ CHANNEL_ID,
+ "Clipper Service",
+ NotificationManager.IMPORTANCE_LOW
+ );
+ channel.setDescription("Keeps Clipper running for clipboard access");
+ channel.setShowBadge(false);
+
+ NotificationManager manager = getSystemService(NotificationManager.class);
+ if (manager != null) {
+ manager.createNotificationChannel(channel);
+ }
+ }
+ }
+
+ private Notification createNotification() {
+ Intent notificationIntent = new Intent(this, Main.class);
+ PendingIntent pendingIntent = PendingIntent.getActivity(
+ this, 0, notificationIntent,
+ PendingIntent.FLAG_IMMUTABLE
+ );
+
+ Notification.Builder builder;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ builder = new Notification.Builder(this, CHANNEL_ID);
+ } else {
+ builder = new Notification.Builder(this);
+ }
+
+ return builder
+ .setContentTitle("Clipper")
+ .setContentText("Clipboard service is running")
+ .setSmallIcon(R.drawable.icon)
+ .setContentIntent(pendingIntent)
+ .setOngoing(true)
+ .build();
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/ca/zgrs/clipper/ClipperReceiver.java b/src/main/java/ca/zgrs/clipper/ClipperReceiver.java
index d833cf8..a8d3d8d 100644
--- a/src/main/java/ca/zgrs/clipper/ClipperReceiver.java
+++ b/src/main/java/ca/zgrs/clipper/ClipperReceiver.java
@@ -1,7 +1,8 @@
package ca.zgrs.clipper;
import android.app.Activity;
-import android.text.ClipboardManager;
+import android.content.ClipboardManager;
+import android.content.ClipData;
import android.content.Context;
import android.content.Intent;
import android.content.BroadcastReceiver;
@@ -36,7 +37,8 @@ public void onReceive(Context context, Intent intent) {
Log.d(TAG, "Setting text into clipboard");
String text = intent.getStringExtra(EXTRA_TEXT);
if (text != null) {
- cb.setText(text);
+ ClipData clip = ClipData.newPlainText("Clipper", text);
+ cb.setPrimaryClip(clip);
setResultCode(Activity.RESULT_OK);
setResultData("Text is copied into clipboard.");
} else {
@@ -45,11 +47,17 @@ public void onReceive(Context context, Intent intent) {
}
} else if (isActionGet(intent.getAction())) {
Log.d(TAG, "Getting text from clipboard");
- CharSequence clip = cb.getText();
- if (clip != null) {
- Log.d(TAG, String.format("Clipboard text: %s", clip));
- setResultCode(Activity.RESULT_OK);
- setResultData(clip.toString());
+ ClipData clipData = cb.getPrimaryClip();
+ if (clipData != null && clipData.getItemCount() > 0) {
+ CharSequence clip = clipData.getItemAt(0).getText();
+ if (clip != null) {
+ Log.d(TAG, String.format("Clipboard text: %s", clip));
+ setResultCode(Activity.RESULT_OK);
+ setResultData(clip.toString());
+ } else {
+ setResultCode(Activity.RESULT_CANCELED);
+ setResultData("");
+ }
} else {
setResultCode(Activity.RESULT_CANCELED);
setResultData("");
diff --git a/src/main/java/ca/zgrs/clipper/Main.java b/src/main/java/ca/zgrs/clipper/Main.java
index cb7aef6..5c539b3 100644
--- a/src/main/java/ca/zgrs/clipper/Main.java
+++ b/src/main/java/ca/zgrs/clipper/Main.java
@@ -1,19 +1,132 @@
package ca.zgrs.clipper;
+import android.Manifest;
import android.app.Activity;
+import android.content.ClipboardManager;
+import android.content.ClipData;
+import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Build;
import android.os.Bundle;
-
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.widget.Toast;
public class Main extends Activity {
+ private static final String TAG = "ClipperMain";
+ private static final int PERMISSION_REQUEST_CODE = 1;
+
+ public static final String ACTION_GET = "ca.zgrs.clipper.GET";
+ public static final String ACTION_SET = "ca.zgrs.clipper.SET";
+
+ private Intent pendingIntent = null;
+ private boolean hasFocus = false;
+ private Handler handler = new Handler(Looper.getMainLooper());
+
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
-
setContentView(R.layout.main);
- // start clipboard service
+ // Store intent for later processing when we have focus
+ pendingIntent = getIntent();
+
+ // Request notification permission on Android 13+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
+ != PackageManager.PERMISSION_GRANTED) {
+ requestPermissions(
+ new String[]{Manifest.permission.POST_NOTIFICATIONS},
+ PERMISSION_REQUEST_CODE
+ );
+ } else {
+ startClipboardService();
+ }
+ } else {
+ startClipboardService();
+ }
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ setIntent(intent);
+ Log.d(TAG, "onNewIntent: " + intent.getAction());
+ pendingIntent = intent;
+ if (hasFocus) {
+ processPendingIntent();
+ }
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasFocus) {
+ super.onWindowFocusChanged(hasFocus);
+ this.hasFocus = hasFocus;
+ Log.d(TAG, "onWindowFocusChanged: " + hasFocus);
+ if (hasFocus && pendingIntent != null) {
+ // Delay to ensure focus is fully established
+ handler.postDelayed(this::processPendingIntent, 500);
+ }
+ }
+
+ private void processPendingIntent() {
+ if (pendingIntent == null) return;
+ Intent intent = pendingIntent;
+ pendingIntent = null;
+ handleClipboardIntent(intent);
+ }
+
+ private void handleClipboardIntent(Intent intent) {
+ if (intent == null) return;
+
+ String action = intent.getAction();
+ if (action == null) return;
+
+ ClipboardManager cb = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
+
+ if (ACTION_SET.equals(action) || "clipper.set".equals(action)) {
+ String text = intent.getStringExtra("text");
+ if (text != null) {
+ Log.d(TAG, "Setting clipboard: " + text);
+ ClipData clip = ClipData.newPlainText("", text);
+ cb.setPrimaryClip(clip);
+ Log.d(TAG, "Clipboard set complete");
+ Toast.makeText(this, "Copied: " + text, Toast.LENGTH_SHORT).show();
+ }
+ } else if (ACTION_GET.equals(action) || "clipper.get".equals(action)) {
+ Log.d(TAG, "Getting clipboard");
+ ClipData clipData = cb.getPrimaryClip();
+ if (clipData != null && clipData.getItemCount() > 0) {
+ CharSequence text = clipData.getItemAt(0).getText();
+ if (text != null) {
+ Log.d(TAG, "Clipboard: " + text);
+ Toast.makeText(this, "Clipboard: " + text, Toast.LENGTH_LONG).show();
+ } else {
+ Log.d(TAG, "Clipboard text is null");
+ Toast.makeText(this, "Clipboard is empty", Toast.LENGTH_SHORT).show();
+ }
+ } else {
+ Log.d(TAG, "No clipboard data");
+ Toast.makeText(this, "Clipboard is empty", Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+ if (requestCode == PERMISSION_REQUEST_CODE) {
+ startClipboardService();
+ }
+ }
+
+ private void startClipboardService() {
Intent serviceIntent = new Intent(this, ClipboardService.class);
- startService(serviceIntent);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ startForegroundService(serviceIntent);
+ } else {
+ startService(serviceIntent);
+ }
}
-}
\ No newline at end of file
+}