Skip to content

Commit 78d30bf

Browse files
author
Your Name
committed
Merge branch 'main' of https://github.com/appdevforall/iiab-android into feat/ADFA-4537-system-locale-picker
2 parents 9ce0637 + f51bc10 commit 78d30bf

9 files changed

Lines changed: 292 additions & 7 deletions

File tree

‎controller/app/src/main/java/org/iiab/controller/MainActivity.java‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,43 @@ public void onCreate(Bundle savedInstanceState) {
302302
}
303303
}).attach();
304304

305+
// ADFA-4538: draggable feedback FAB — present on all tabs (it lives in the activity,
306+
// above the ViewPager, so it persists across tab switches). Tap -> capture screenshot
307+
// -> open the feedback form tagged with the current tab.
308+
com.google.android.material.floatingactionbutton.FloatingActionButton fabFeedback =
309+
findViewById(R.id.fab_feedback);
310+
org.iiab.controller.feedback.presentation.FeedbackFab.attach(fabFeedback, () -> {
311+
int pos = viewPager.getCurrentItem();
312+
String screen = pos == 0 ? "status" : pos == 1 ? "usage" : pos == 2 ? "deploy"
313+
: pos == 3 ? "share" : "main";
314+
// Direct feedback report (no intermediate form): capture -> build a report with
315+
// device/rootfs diagnostics + screenshot -> hand to the configured transport, which
316+
// opens the mail chooser (where the user can add a note or cancel).
317+
org.iiab.controller.feedback.data.FeedbackScreenshot.capture(MainActivity.this, path -> {
318+
org.iiab.controller.feedback.domain.FeedbackPayload payload =
319+
org.iiab.controller.feedback.domain.FeedbackPayload
320+
.builder(org.iiab.controller.feedback.domain.FeedbackType.GENERAL)
321+
.appVersion(org.iiab.controller.feedback.data.FeedbackDiagnostics.appVersionName(MainActivity.this))
322+
.appBuild(org.iiab.controller.feedback.data.FeedbackDiagnostics.appVersionCode(MainActivity.this))
323+
.androidRelease(android.os.Build.VERSION.RELEASE)
324+
.device(android.os.Build.MANUFACTURER + " " + android.os.Build.MODEL)
325+
.abi(new org.iiab.controller.deviceinfo.domain.GetDeviceArchUseCase(
326+
new org.iiab.controller.deviceinfo.data.BuildDeviceAbiProvider()).execute())
327+
.binariesTag(org.iiab.controller.feedback.data.FeedbackDiagnostics.binariesTag(MainActivity.this))
328+
.screen("main." + screen)
329+
.screenshot(path)
330+
.build();
331+
boolean ok = org.iiab.controller.feedback.data.FeedbackConfig.create(MainActivity.this)
332+
.send(MainActivity.this, payload);
333+
if (ok) {
334+
org.iiab.controller.analytics.AnalyticsClient.with(MainActivity.this).logFeedbackSent();
335+
} else {
336+
android.widget.Toast.makeText(MainActivity.this, R.string.feedback_no_email_app,
337+
android.widget.Toast.LENGTH_LONG).show();
338+
}
339+
});
340+
});
341+
305342
// --- TUTORIAL TAB DETECTOR ---
306343
viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
307344
@Override

‎controller/app/src/main/java/org/iiab/controller/feedback/data/EmailFeedbackSender.java‎

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,30 @@ public final class EmailFeedbackSender {
1919

2020
/** @return true if a mail app handled the intent; false if none is available. */
2121
public boolean send(Context ctx, EmailContent email) {
22-
Intent intent = new Intent(Intent.ACTION_SENDTO);
23-
intent.setData(Uri.parse("mailto:"));
24-
intent.putExtra(Intent.EXTRA_EMAIL, new String[]{email.recipient()});
25-
intent.putExtra(Intent.EXTRA_SUBJECT, email.subject());
26-
intent.putExtra(Intent.EXTRA_TEXT, email.body());
2722
try {
23+
Intent intent;
24+
if (email.attachmentPath() != null) {
25+
// ADFA-4538: attach the screenshot via ACTION_SEND (mailto ignores attachments).
26+
java.io.File file = new java.io.File(email.attachmentPath());
27+
Uri uri = androidx.core.content.FileProvider.getUriForFile(
28+
ctx, ctx.getPackageName() + ".provider", file);
29+
intent = new Intent(Intent.ACTION_SEND);
30+
intent.setType("message/rfc822");
31+
intent.putExtra(Intent.EXTRA_STREAM, uri);
32+
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
33+
} else {
34+
intent = new Intent(Intent.ACTION_SENDTO);
35+
intent.setData(Uri.parse("mailto:"));
36+
}
37+
intent.putExtra(Intent.EXTRA_EMAIL, new String[]{email.recipient()});
38+
intent.putExtra(Intent.EXTRA_SUBJECT, email.subject());
39+
intent.putExtra(Intent.EXTRA_TEXT, email.body());
2840
ctx.startActivity(Intent.createChooser(intent, ctx.getString(R.string.feedback_send)));
2941
return true;
3042
} catch (ActivityNotFoundException e) {
3143
return false;
44+
} catch (Throwable t) {
45+
return false;
3246
}
3347
}
3448
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package org.iiab.controller.feedback.data;
2+
3+
import android.app.Activity;
4+
import android.content.Context;
5+
import android.graphics.Bitmap;
6+
import android.graphics.Canvas;
7+
import android.os.Build;
8+
import android.os.Handler;
9+
import android.os.Looper;
10+
import android.view.PixelCopy;
11+
import android.view.View;
12+
13+
import java.io.File;
14+
import java.io.FileOutputStream;
15+
16+
/**
17+
* Captures the current window as a JPEG under {@code filesDir/feedback_screenshots}
18+
* (recycled from Code On the Go). Async: the result path is delivered on the main
19+
* thread; {@code null} means capture/save failed.
20+
*
21+
* <p>Uses {@link PixelCopy} on API 26+ (accurate, hardware-accelerated content); falls
22+
* back to drawing the view hierarchy to a {@link Canvas} on API 24-25 (minSdk 24).
23+
* ADFA-4538.
24+
*/
25+
public final class FeedbackScreenshot {
26+
27+
public interface Callback {
28+
void onResult(String path);
29+
}
30+
31+
private FeedbackScreenshot() {
32+
}
33+
34+
public static void capture(Activity activity, Callback cb) {
35+
try {
36+
View root = activity.getWindow().getDecorView().getRootView();
37+
int w = root.getWidth();
38+
int h = root.getHeight();
39+
if (w <= 0 || h <= 0) {
40+
cb.onResult(null);
41+
return;
42+
}
43+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
44+
Bitmap bmp = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
45+
PixelCopy.request(activity.getWindow(), bmp, result -> {
46+
if (result == PixelCopy.SUCCESS) {
47+
cb.onResult(save(activity, bmp));
48+
} else {
49+
cb.onResult(drawToBitmap(activity, root, w, h));
50+
}
51+
}, new Handler(Looper.getMainLooper()));
52+
} else {
53+
cb.onResult(drawToBitmap(activity, root, w, h));
54+
}
55+
} catch (Throwable t) {
56+
cb.onResult(null);
57+
}
58+
}
59+
60+
/** API 24-25 fallback: render the view hierarchy into a bitmap. */
61+
private static String drawToBitmap(Context ctx, View root, int w, int h) {
62+
try {
63+
Bitmap bmp = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
64+
root.draw(new Canvas(bmp));
65+
return save(ctx, bmp);
66+
} catch (Throwable t) {
67+
return null;
68+
}
69+
}
70+
71+
private static String save(Context ctx, Bitmap bmp) {
72+
try {
73+
File dir = new File(ctx.getFilesDir(), "feedback_screenshots");
74+
dir.mkdirs();
75+
File f = new File(dir, "screenshot_" + System.currentTimeMillis() + ".jpg");
76+
try (FileOutputStream out = new FileOutputStream(f)) {
77+
bmp.compress(Bitmap.CompressFormat.JPEG, 80, out);
78+
}
79+
return f.getAbsolutePath();
80+
} catch (Throwable t) {
81+
return null;
82+
}
83+
}
84+
}

‎controller/app/src/main/java/org/iiab/controller/feedback/domain/EmailContent.java‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,25 @@ public final class EmailContent {
1010
private final String recipient;
1111
private final String subject;
1212
private final String body;
13+
private final String attachmentPath;
1314

1415
public EmailContent(String recipient, String subject, String body) {
16+
this(recipient, subject, body, null);
17+
}
18+
19+
public EmailContent(String recipient, String subject, String body, String attachmentPath) {
1520
this.recipient = recipient;
1621
this.subject = subject;
1722
this.body = body;
23+
this.attachmentPath = attachmentPath;
1824
}
1925

2026
public String recipient() { return recipient; }
2127

2228
public String subject() { return subject; }
2329

2430
public String body() { return body; }
31+
32+
/** Optional local file path to attach; null when there is no attachment. */
33+
public String attachmentPath() { return attachmentPath; }
2534
}

‎controller/app/src/main/java/org/iiab/controller/feedback/domain/FeedbackPayload.java‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public final class FeedbackPayload {
2727
private final String binariesTag;
2828
private final String screen;
2929
private final String stackTrace;
30+
private final String screenshot;
3031

3132
private FeedbackPayload(Builder b) {
3233
if (b.type == null) {
@@ -45,6 +46,7 @@ private FeedbackPayload(Builder b) {
4546
this.binariesTag = b.binariesTag;
4647
this.screen = b.screen;
4748
this.stackTrace = b.stackTrace;
49+
this.screenshot = b.screenshot;
4850
}
4951

5052
public FeedbackType type() { return type; }
@@ -73,6 +75,9 @@ private FeedbackPayload(Builder b) {
7375

7476
public String stackTrace() { return stackTrace; }
7577

78+
/** Absolute path to an optional screenshot to attach (local file). */
79+
public String screenshot() { return screenshot; }
80+
7681
public static Builder builder(FeedbackType type) {
7782
return new Builder(type);
7883
}
@@ -92,6 +97,7 @@ public static final class Builder {
9297
private String binariesTag;
9398
private String screen;
9499
private String stackTrace;
100+
private String screenshot;
95101

96102
private Builder(FeedbackType type) {
97103
this.type = type;
@@ -121,6 +127,8 @@ private Builder(FeedbackType type) {
121127

122128
public Builder stackTrace(String v) { this.stackTrace = v; return this; }
123129

130+
public Builder screenshot(String v) { this.screenshot = v; return this; }
131+
124132
public FeedbackPayload build() {
125133
return new FeedbackPayload(this);
126134
}

‎controller/app/src/main/java/org/iiab/controller/feedback/domain/FeedbackRenderer.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public final class FeedbackRenderer {
1919
private static final String DASH = " \u2014 ";
2020

2121
public EmailContent render(FeedbackPayload p) {
22-
return new EmailContent(RECIPIENT, subject(p), body(p));
22+
return new EmailContent(RECIPIENT, subject(p), body(p), p.screenshot());
2323
}
2424

2525
private String subject(FeedbackPayload p) {

‎controller/app/src/main/java/org/iiab/controller/feedback/domain/FeedbackType.java‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ public enum FeedbackType {
1111
CRASH("Crash"),
1212
BUG("Bug"),
1313
SUGGESTION("Suggestion"),
14-
CONTENT_REQUEST("Content request");
14+
CONTENT_REQUEST("Content request"),
15+
GENERAL("Feedback");
1516

1617
private final String label;
1718

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package org.iiab.controller.feedback.presentation;
2+
3+
import android.annotation.SuppressLint;
4+
import android.content.Context;
5+
import android.content.SharedPreferences;
6+
import android.view.MotionEvent;
7+
import android.view.View;
8+
import android.view.ViewConfiguration;
9+
import android.view.ViewGroup;
10+
11+
import com.google.android.material.floatingactionbutton.FloatingActionButton;
12+
13+
/**
14+
* Draggable feedback FAB with position persistence, recycled from Code On the Go's
15+
* FeedbackButtonManager. Distinguishes a tap (fires {@code onTap}) from a drag (moves
16+
* the button and stores its position as normalized ratios). ADFA-4538.
17+
*/
18+
public final class FeedbackFab {
19+
20+
private static final String PREFS = "iiab_delivery";
21+
private static final String KEY_X = "fab_x_ratio";
22+
private static final String KEY_Y = "fab_y_ratio";
23+
24+
private FeedbackFab() {
25+
}
26+
27+
@SuppressLint("ClickableViewAccessibility")
28+
public static void attach(FloatingActionButton fab, Runnable onTap) {
29+
if (fab == null) {
30+
return;
31+
}
32+
final Context ctx = fab.getContext();
33+
final int slop = ViewConfiguration.get(ctx).getScaledTouchSlop();
34+
35+
fab.post(() -> applySaved(fab));
36+
fab.addOnLayoutChangeListener((v, l, t, r, b, ol, ot, orr, ob) -> applySaved(fab));
37+
38+
fab.setOnTouchListener(new View.OnTouchListener() {
39+
float downX, downY, dX, dY;
40+
boolean dragging;
41+
42+
@Override
43+
public boolean onTouch(View v, MotionEvent e) {
44+
switch (e.getActionMasked()) {
45+
case MotionEvent.ACTION_DOWN:
46+
downX = e.getRawX();
47+
downY = e.getRawY();
48+
dX = v.getX() - e.getRawX();
49+
dY = v.getY() - e.getRawY();
50+
dragging = false;
51+
return true;
52+
case MotionEvent.ACTION_MOVE:
53+
if (!dragging && Math.hypot(e.getRawX() - downX, e.getRawY() - downY) > slop) {
54+
dragging = true;
55+
}
56+
if (dragging) {
57+
ViewGroup parent = (ViewGroup) v.getParent();
58+
if (parent == null) {
59+
return true;
60+
}
61+
float nx = clamp(e.getRawX() + dX, 0, parent.getWidth() - v.getWidth());
62+
float ny = clamp(e.getRawY() + dY, 0, parent.getHeight() - v.getHeight());
63+
v.setX(nx);
64+
v.setY(ny);
65+
}
66+
return true;
67+
case MotionEvent.ACTION_UP:
68+
if (dragging) {
69+
savePosition(fab);
70+
} else {
71+
v.performClick();
72+
if (onTap != null) {
73+
onTap.run();
74+
}
75+
}
76+
return true;
77+
default:
78+
return false;
79+
}
80+
}
81+
});
82+
}
83+
84+
private static float clamp(float v, float lo, float hi) {
85+
if (hi < lo) {
86+
return lo;
87+
}
88+
return Math.max(lo, Math.min(v, hi));
89+
}
90+
91+
private static void applySaved(FloatingActionButton fab) {
92+
ViewGroup parent = (ViewGroup) fab.getParent();
93+
if (parent == null || parent.getWidth() == 0 || parent.getHeight() == 0) {
94+
return;
95+
}
96+
SharedPreferences p = fab.getContext().getSharedPreferences(PREFS, Context.MODE_PRIVATE);
97+
float xr = p.getFloat(KEY_X, -1f);
98+
float yr = p.getFloat(KEY_Y, -1f);
99+
if (xr < 0 || yr < 0) {
100+
return;
101+
}
102+
float availX = parent.getWidth() - fab.getWidth();
103+
float availY = parent.getHeight() - fab.getHeight();
104+
fab.setX(clamp(xr * availX, 0, availX));
105+
fab.setY(clamp(yr * availY, 0, availY));
106+
}
107+
108+
private static void savePosition(FloatingActionButton fab) {
109+
ViewGroup parent = (ViewGroup) fab.getParent();
110+
if (parent == null) {
111+
return;
112+
}
113+
float availX = parent.getWidth() - fab.getWidth();
114+
float availY = parent.getHeight() - fab.getHeight();
115+
if (availX <= 0 || availY <= 0) {
116+
return;
117+
}
118+
fab.getContext().getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit()
119+
.putFloat(KEY_X, clamp(fab.getX() / availX, 0, 1))
120+
.putFloat(KEY_Y, clamp(fab.getY() / availY, 0, 1))
121+
.apply();
122+
}
123+
}

‎controller/app/src/main/res/layout/main.xml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,4 +208,13 @@
208208
android:visibility="visible" />
209209
</LinearLayout>
210210

211+
<com.google.android.material.floatingactionbutton.FloatingActionButton
212+
android:id="@+id/fab_feedback"
213+
android:layout_width="wrap_content"
214+
android:layout_height="wrap_content"
215+
android:layout_gravity="bottom|end"
216+
android:layout_margin="16dp"
217+
android:contentDescription="@string/feedback_send"
218+
android:src="@drawable/ic_feedback_24" />
219+
211220
</androidx.coordinatorlayout.widget.CoordinatorLayout>

0 commit comments

Comments
 (0)