Skip to content

Commit f5ba572

Browse files
committed
feat(Android): Shake to share
Implement shake to share for android + migrate to an event driven mechanism for IOS as well
1 parent ad12fff commit f5ba572

File tree

8 files changed

+327
-35
lines changed

8 files changed

+327
-35
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package app.status.mobile;
2+
3+
import android.app.Activity;
4+
import android.content.Context;
5+
import android.hardware.Sensor;
6+
import android.hardware.SensorEvent;
7+
import android.hardware.SensorEventListener;
8+
import android.hardware.SensorManager;
9+
import android.os.SystemClock;
10+
import android.util.Log;
11+
12+
import java.util.concurrent.atomic.AtomicBoolean;
13+
14+
public final class ShakeDetector {
15+
private static final String TAG = "StatusShake";
16+
private static final AtomicBoolean started = new AtomicBoolean(false);
17+
private static final AtomicBoolean registered = new AtomicBoolean(false);
18+
private static SensorManager sensorManager;
19+
private static Sensor accelerometer;
20+
private static long lastShakeMs = 0L;
21+
22+
// Threshold and cooldown tuned to match iOS behavior.
23+
private static final float SHAKE_THRESHOLD = 1.35f; // delta from 1g
24+
private static final long COOLDOWN_MS = 1000;
25+
26+
private static final SensorEventListener listener = new SensorEventListener() {
27+
@Override
28+
public void onSensorChanged(SensorEvent event) {
29+
if (event == null || event.values == null || event.values.length < 3) return;
30+
31+
float ax = event.values[0];
32+
float ay = event.values[1];
33+
float az = event.values[2];
34+
35+
float g = (float) Math.sqrt(ax * ax + ay * ay + az * az) / SensorManager.GRAVITY_EARTH;
36+
float deltaFrom1g = Math.abs(g - 1.0f);
37+
38+
if (deltaFrom1g < SHAKE_THRESHOLD) return;
39+
40+
long now = SystemClock.elapsedRealtime();
41+
if (now - lastShakeMs < COOLDOWN_MS) return;
42+
43+
lastShakeMs = now;
44+
Log.i(TAG, "detected: g=" + g + " deltaFrom1g=" + deltaFrom1g);
45+
nativeShakeDetected();
46+
}
47+
48+
@Override
49+
public void onAccuracyChanged(Sensor sensor, int accuracy) {
50+
// no-op
51+
}
52+
};
53+
54+
private ShakeDetector() {}
55+
56+
public static void start(Activity activity) {
57+
if (activity == null) return;
58+
59+
if (!started.compareAndSet(false, true)) {
60+
onResume(activity);
61+
return;
62+
}
63+
64+
Context appContext = activity.getApplicationContext();
65+
sensorManager = (SensorManager) appContext.getSystemService(Context.SENSOR_SERVICE);
66+
if (sensorManager == null) {
67+
Log.w(TAG, "SensorManager unavailable");
68+
return;
69+
}
70+
71+
accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
72+
if (accelerometer == null) {
73+
Log.w(TAG, "Accelerometer not available");
74+
return;
75+
}
76+
77+
onResume(activity);
78+
}
79+
80+
public static void onResume(Activity activity) {
81+
if (sensorManager == null || accelerometer == null) {
82+
if (activity != null) {
83+
start(activity);
84+
}
85+
return;
86+
}
87+
if (registered.compareAndSet(false, true)) {
88+
sensorManager.registerListener(listener, accelerometer, SensorManager.SENSOR_DELAY_GAME);
89+
}
90+
}
91+
92+
public static void onPause() {
93+
if (sensorManager == null || accelerometer == null) return;
94+
if (registered.compareAndSet(true, false)) {
95+
sensorManager.unregisterListener(listener, accelerometer);
96+
}
97+
}
98+
99+
private static native void nativeShakeDetected();
100+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package app.status.mobile;
2+
3+
import android.app.Activity;
4+
import android.content.Intent;
5+
import android.net.Uri;
6+
import android.util.Log;
7+
8+
import androidx.core.content.FileProvider;
9+
10+
import java.io.File;
11+
import java.util.ArrayList;
12+
13+
public final class ShareUtils {
14+
private static final String TAG = "StatusShare";
15+
16+
private ShareUtils() {}
17+
18+
public static void sharePaths(Activity activity, ArrayList<String> paths) {
19+
if (activity == null || paths == null || paths.isEmpty()) return;
20+
21+
String authority = activity.getPackageName() + ".qtprovider";
22+
ArrayList<Uri> uris = new ArrayList<>();
23+
24+
for (String path : paths) {
25+
if (path == null || path.isEmpty()) continue;
26+
Uri uri = toUri(activity, authority, path);
27+
if (uri != null) {
28+
uris.add(uri);
29+
}
30+
}
31+
32+
if (uris.isEmpty()) return;
33+
34+
Intent intent;
35+
if (uris.size() == 1) {
36+
intent = new Intent(Intent.ACTION_SEND);
37+
intent.putExtra(Intent.EXTRA_STREAM, uris.get(0));
38+
} else {
39+
intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
40+
intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
41+
}
42+
43+
intent.setType("*/*");
44+
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
45+
46+
try {
47+
activity.startActivity(Intent.createChooser(intent, null));
48+
} catch (Throwable t) {
49+
Log.e(TAG, "sharePaths failed", t);
50+
}
51+
}
52+
53+
private static Uri toUri(Activity activity, String authority, String path) {
54+
try {
55+
if (path.startsWith("content://")) {
56+
return Uri.parse(path);
57+
}
58+
if (path.startsWith("file://")) {
59+
Uri parsed = Uri.parse(path);
60+
path = parsed.getPath();
61+
}
62+
if (path == null || path.isEmpty()) return null;
63+
64+
File file = new File(path);
65+
if (!file.exists()) {
66+
Log.w(TAG, "sharePaths: file missing: " + path);
67+
}
68+
return FileProvider.getUriForFile(activity, authority, file);
69+
} catch (Throwable t) {
70+
Log.e(TAG, "sharePaths: failed to build uri for " + path, t);
71+
return null;
72+
}
73+
}
74+
}

mobile/android/qt6/src/app/status/mobile/StatusQtActivity.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,21 @@ public void onCreate(Bundle savedInstanceState) {
2525
// QTBUG-140897: Install Android 16 keyboard workaround
2626
// Remove this line when Qt 6.10+ fixes the issue
2727
mKeyboardWorkaround = Android16KeyboardWorkaround.install(this);
28+
29+
// Set up shake detection (used for share-on-shake)
30+
ShakeDetector.start(this);
31+
}
32+
33+
@Override
34+
protected void onResume() {
35+
super.onResume();
36+
ShakeDetector.onResume(this);
37+
}
38+
39+
@Override
40+
protected void onPause() {
41+
ShakeDetector.onPause();
42+
super.onPause();
2843
}
2944

3045
@Override

ui/StatusQ/include/StatusQ/systemutilsinternal.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ class SystemUtilsInternal : public QObject
4545
Q_INVOKABLE void iosShareFiles(const QVariantList& fileUrls) const;
4646
Q_INVOKABLE void iosSharePaths(const QStringList& filePaths) const;
4747

48+
// Android native share sheet
49+
Q_INVOKABLE void androidSharePaths(const QStringList& filePaths) const;
50+
4851
// Debug helper (used from QML to verify signal handlers fire on device)
4952
Q_INVOKABLE void debugLog(const QString& message) const;
5053

@@ -65,6 +68,4 @@ class SystemUtilsInternal : public QObject
6568
int m_iosKeyboardHeight = 0;
6669
bool m_iosKeyboardVisible = false;
6770
QTimer* m_iosKeyboardPollTimer = nullptr;
68-
QTimer* m_iosShakePollTimer = nullptr;
69-
int m_iosShakeCount = 0;
7071
};

ui/StatusQ/src/ios_utils.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ bool isIOSKeyboardVisible();
1313

1414
// Shake detection utilities
1515
void setupIOSShakeDetection();
16-
int getIOSShakeCount();
16+
using IOSShakeCallback = void (*)();
17+
void setIOSShakeCallback(IOSShakeCallback callback);
1718

1819
// Share sheet utilities
1920
void presentIOSShareSheetForFilePath(const QString& filePath);

ui/StatusQ/src/ios_utils.mm

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -285,9 +285,13 @@ bool isIOSKeyboardVisible() {
285285
// Shake detection
286286
// -----------------------------------------------------------------------------
287287

288-
static std::atomic<int> g_shakeCount{0};
289288
static std::atomic<bool> g_shakeDetectionStarted{false};
290289
static CMMotionManager* g_motionManager = nil;
290+
static IOSShakeCallback g_shakeCallback = nullptr;
291+
292+
void setIOSShakeCallback(IOSShakeCallback callback) {
293+
g_shakeCallback = callback;
294+
}
291295

292296
void setupIOSShakeDetection() {
293297
@autoreleasepool {
@@ -337,16 +341,14 @@ void setupIOSShakeDetection() {
337341
if (nowTs - lastShakeTs < kCooldownSec) return;
338342

339343
lastShakeTs = nowTs;
340-
const int newCount = g_shakeCount.fetch_add(1) + 1;
341-
NSLog(@"[iOS Shake] detected: count=%d mag=%f deltaFrom1g=%f", newCount, mag, deltaFrom1g);
344+
NSLog(@"[iOS Shake] detected: mag=%f deltaFrom1g=%f", mag, deltaFrom1g);
345+
if (g_shakeCallback) {
346+
g_shakeCallback();
347+
}
342348
}];
343349
}
344350
}
345351

346-
int getIOSShakeCount() {
347-
return g_shakeCount.load();
348-
}
349-
350352
// -----------------------------------------------------------------------------
351353
// Share sheet
352354
// -----------------------------------------------------------------------------

0 commit comments

Comments
 (0)