Skip to content

Commit 6d645d0

Browse files
authored
Merge fad/next into master to prepare for next release (#4784)
* Some App Distribution javadoc updates (#4740) * Support JPGs (in addition to PNGs) as feedback screenshots and set correct content-type/filename (#4783) * Support PNGs as feedback screenshots * Add tests and inject ContentResolver * Address feedback * Flip order of conditional * Update changelog and version for M129
1 parent 0a73fdb commit 6d645d0

File tree

12 files changed

+397
-105
lines changed

12 files changed

+397
-105
lines changed

firebase-appdistribution-api/src/main/java/com/google/firebase/appdistribution/FirebaseAppDistribution.java

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public interface FirebaseAppDistribution {
4343
* following actions:
4444
*
4545
* <ol>
46-
* <li>If tester is not signed in, presents the tester with a Google Sign-in UI.
46+
* <li>If the tester is not signed in, presents the tester with a Google Sign-in UI.
4747
* <li>Checks if a newer release is available. If so, presents the tester with a confirmation
4848
* dialog to begin the download.
4949
* <li>If the newest release is an APK, downloads the binary and starts an installation. If the
@@ -119,9 +119,9 @@ public interface FirebaseAppDistribution {
119119
* <p>Performs the following actions:
120120
*
121121
* <ol>
122-
* <li>Takes a screenshot of the current activity
123-
* <li>If tester is not signed in, presents the tester with a Google Sign-in UI
124-
* <li>Starts a full screen activity for the tester to compose and submit the feedback
122+
* <li>Takes a screenshot of the current activity.
123+
* <li>If the tester is not signed in, presents the tester with a Google Sign-in UI.
124+
* <li>Starts a full screen activity for the tester to compose and submit the feedback.
125125
* </ol>
126126
*
127127
* @param additionalFormText string resource ID of text that will be shown to the tester before
@@ -137,9 +137,9 @@ public interface FirebaseAppDistribution {
137137
* <p>Performs the following actions:
138138
*
139139
* <ol>
140-
* <li>Takes a screenshot of the current activity
141-
* <li>If tester is not signed in, presents the tester with a Google Sign-in UI
142-
* <li>Starts a full screen activity for the tester to compose and submit the feedback
140+
* <li>Takes a screenshot of the current activity.
141+
* <li>If the tester is not signed in, presents the tester with a Google Sign-in UI.
142+
* <li>Starts a full screen activity for the tester to compose and submit the feedback.
143143
* </ol>
144144
*
145145
* @param additionalFormText text that will be shown to the tester before they submit feedback. If
@@ -155,8 +155,8 @@ public interface FirebaseAppDistribution {
155155
* <p>Performs the following actions:
156156
*
157157
* <ol>
158-
* <li>If tester is not signed in, presents the tester with a Google Sign-in UI
159-
* <li>Starts a full screen activity for the tester to compose and submit the feedback
158+
* <li>If the tester is not signed in, presents the tester with a Google Sign-in UI.
159+
* <li>Starts a full screen activity for the tester to compose and submit the feedback.
160160
* </ol>
161161
*
162162
* @param additionalFormText string resource ID of text that will be shown to the tester before
@@ -175,8 +175,8 @@ public interface FirebaseAppDistribution {
175175
* <p>Performs the following actions:
176176
*
177177
* <ol>
178-
* <li>If tester is not signed in, presents the tester with a Google Sign-in UI
179-
* <li>Starts a full screen activity for the tester to compose and submit the feedback
178+
* <li>If the tester is not signed in, presents the tester with a Google Sign-in UI.
179+
* <li>Starts a full screen activity for the tester to compose and submit the feedback.
180180
* </ol>
181181
*
182182
* @param additionalFormText text that will be shown to the tester before they submit feedback. If
@@ -191,28 +191,29 @@ public interface FirebaseAppDistribution {
191191
* Displays a notification that, when tapped, will take a screenshot of the current activity, then
192192
* start a new activity to collect and submit feedback from the tester along with the screenshot.
193193
*
194-
* <p>On Android 13 and above, this method requires the <a
195-
* href="https://developer.android.com/develop/ui/views/notifications/notification-permission">runtime
196-
* permission for sending notifications</a>: {@code POST_NOTIFICATIONS}. If your app targets
197-
* Android 13 (API level 33) or above, you should <a
194+
* <p>On Android 13 and above, this method requires the runtime permission for sending
195+
* notifications: <a
196+
* href="https://developer.android.com/develop/ui/views/notifications/notification-permission">{@code
197+
* POST_NOTIFICATIONS}</a>. If your app targets Android 13 (API level 33) or above, you should <a
198198
* href="https://developer.android.com/training/permissions/requesting">request the
199199
* permission</a>.
200200
*
201201
* <p>When the notification is tapped:
202202
*
203203
* <ol>
204-
* <li>If the app is open, take a screenshot of the current activity
205-
* <li>If tester is not signed in, presents the tester with a Google Sign-in UI
206-
* <li>Starts a full screen activity for the tester to compose and submit the feedback
204+
* <li>If the app is open, takes a screenshot of the current activity.
205+
* <li>If the tester is not signed in, presents the tester with a Google Sign-in UI.
206+
* <li>Starts a full screen activity for the tester to compose and submit the feedback.
207207
* </ol>
208208
*
209209
* @param additionalFormText string resource ID of text that will be shown to the tester before
210210
* they submit feedback. If you’re a customer who would like to provide notice to your testers
211211
* about collection and processing of their feedback data, you can use this text to provide
212212
* such notice.
213213
* @param interruptionLevel the level of interruption for the feedback notification. On platforms
214-
* below Android 8, this corresponds to a notification channel importance and once set cannot
215-
* be changed except by the user.
214+
* below Android 8, this corresponds to a <a
215+
* href="https://developer.android.com/develop/ui/views/notifications/channels#importance">notification
216+
* channel importance</a> and once set cannot be changed except by the user.
216217
*/
217218
void showFeedbackNotification(
218219
@StringRes int additionalFormText, @NonNull InterruptionLevel interruptionLevel);
@@ -221,32 +222,36 @@ void showFeedbackNotification(
221222
* Displays a notification that, when tapped, will take a screenshot of the current activity, then
222223
* start a new activity to collect and submit feedback from the tester along with the screenshot.
223224
*
224-
* <p>On Android 13 and above, this method requires the <a
225-
* href="https://developer.android.com/develop/ui/views/notifications/notification-permission">runtime
226-
* permission for sending notifications</a>: {@code POST_NOTIFICATIONS}. If your app targets
227-
* Android 13 (API level 33) or above, you should <a
225+
* <p>On Android 13 and above, this method requires the runtime permission for sending
226+
* notifications: <a
227+
* href="https://developer.android.com/develop/ui/views/notifications/notification-permission">{@code
228+
* POST_NOTIFICATIONS}</a>. If your app targets Android 13 (API level 33) or above, you should <a
228229
* href="https://developer.android.com/training/permissions/requesting">request the
229230
* permission</a>.
230231
*
231232
* <p>When the notification is tapped:
232233
*
233234
* <ol>
234-
* <li>If the app is open, take a screenshot of the current activity
235-
* <li>If tester is not signed in, presents the tester with a Google Sign-in UI
236-
* <li>Starts a full screen activity for the tester to compose and submit the feedback
235+
* <li>If the app is open, takes a screenshot of the current activity.
236+
* <li>If the tester is not signed in, presents the tester with a Google Sign-in UI.
237+
* <li>Starts a full screen activity for the tester to compose and submit the feedback.
237238
* </ol>
238239
*
239240
* @param additionalFormText text that will be shown to the tester before they submit feedback. If
240241
* you’re a customer who would like to provide notice to your testers about collection and
241242
* processing of their feedback data, you can use this text to provide such notice.
242243
* @param interruptionLevel the level of interruption for the feedback notification. On platforms
243-
* below Android 8, this corresponds to a notification channel importance and once set cannot
244-
* be changed except by the user.
244+
* below Android 8, this corresponds to a <a
245+
* href="https://developer.android.com/develop/ui/views/notifications/channels#importance">notification
246+
* channel importance</a> and once set cannot be changed except by the user.
245247
*/
246248
void showFeedbackNotification(
247249
@NonNull CharSequence additionalFormText, @NonNull InterruptionLevel interruptionLevel);
248250

249-
/** Hides the notification shown with {@link #showFeedbackNotification}. */
251+
/**
252+
* Hides the notification shown with {@link #showFeedbackNotification(int, InterruptionLevel)} or
253+
* {@link #showFeedbackNotification(CharSequence, InterruptionLevel)}.
254+
*/
250255
void cancelFeedbackNotification();
251256

252257
/** Gets the singleton {@link FirebaseAppDistribution} instance. */

firebase-appdistribution/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
# Unreleased
2+
* [feature] Adds support for attaching JPEG screenshots to tester feedback.
3+
4+
# 16.0.0-beta06
25
* [feature] Adds support for in-app tester feedback. To learn more, see
36
[Collect feedback from testers](/docs/app-distribution/collect-feedback-from-testers).
47
* [fixed] Fixed a bug where only the last listener added to an `UpdateTask` using

firebase-appdistribution/gradle.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
version=16.0.0-beta06
16-
latestReleasedVersion=16.0.0-beta05
15+
version=16.0.0-beta07
16+
latestReleasedVersion=16.0.0-beta06

firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/AppDistroComponent.java

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
package com.google.firebase.appdistribution.impl;
1616

17+
import android.content.ContentResolver;
1718
import android.content.Context;
1819
import com.google.firebase.FirebaseApp;
1920
import com.google.firebase.FirebaseOptions;
@@ -28,6 +29,7 @@
2829
import dagger.BindsInstance;
2930
import dagger.Component;
3031
import dagger.Module;
32+
import dagger.Provides;
3133
import java.util.concurrent.Executor;
3234
import java.util.concurrent.ExecutorService;
3335
import java.util.concurrent.ScheduledExecutorService;
@@ -85,32 +87,39 @@ interface Builder {
8587
void inject(TakeScreenshotAndStartFeedbackActivity activity);
8688

8789
@Module
88-
interface MainModule {
90+
abstract class MainModule {
8991
@Binds
90-
FirebaseAppDistribution bindAppDistro(FirebaseAppDistributionImpl impl);
92+
abstract FirebaseAppDistribution bindAppDistro(FirebaseAppDistributionImpl impl);
9193

9294
@Binds
9395
@Background
94-
ExecutorService bindBackgroundExecutorService(@Background ScheduledExecutorService ses);
96+
abstract ExecutorService bindBackgroundExecutorService(
97+
@Background ScheduledExecutorService ses);
9598

9699
@Binds
97100
@Background
98-
Executor bindBackgroundExecutor(@Background ExecutorService es);
101+
abstract Executor bindBackgroundExecutor(@Background ExecutorService es);
99102

100103
@Binds
101104
@Lightweight
102-
ExecutorService bindLightweightExecutorService(@Lightweight ScheduledExecutorService ses);
105+
abstract ExecutorService bindLightweightExecutorService(
106+
@Lightweight ScheduledExecutorService ses);
103107

104108
@Binds
105109
@Lightweight
106-
Executor bindLightweightExecutor(@Lightweight ExecutorService es);
110+
abstract Executor bindLightweightExecutor(@Lightweight ExecutorService es);
107111

108112
@Binds
109113
@Blocking
110-
ExecutorService bindBlockingExecutorService(@Blocking ScheduledExecutorService ses);
114+
abstract ExecutorService bindBlockingExecutorService(@Blocking ScheduledExecutorService ses);
111115

112116
@Binds
113117
@Blocking
114-
Executor bindBlockingExecutor(@Blocking ExecutorService es);
118+
abstract Executor bindBlockingExecutor(@Blocking ExecutorService es);
119+
120+
@Provides
121+
static ContentResolver bindContentResolver(Context applicationContext) {
122+
return applicationContext.getContentResolver();
123+
}
115124
}
116125
}

firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/FeedbackActivity.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,8 @@ private void setupView() {
126126
v -> {
127127
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
128128
intent.addCategory(Intent.CATEGORY_OPENABLE);
129-
intent.setType("image/png");
129+
intent.setType("*/*");
130+
intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {"image/png", "image/jpeg"});
130131
chooseScreenshotLauncher.launch(intent);
131132
});
132133

firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/FeedbackSender.java

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,40 @@
1414

1515
package com.google.firebase.appdistribution.impl;
1616

17+
import android.content.ContentResolver;
18+
import android.database.Cursor;
1719
import android.net.Uri;
20+
import android.provider.OpenableColumns;
1821
import androidx.annotation.Nullable;
1922
import com.google.android.gms.tasks.Task;
2023
import com.google.android.gms.tasks.Tasks;
2124
import com.google.firebase.annotations.concurrent.Lightweight;
25+
import com.google.firebase.appdistribution.FirebaseAppDistributionException;
26+
import com.google.firebase.appdistribution.FirebaseAppDistributionException.Status;
2227
import java.util.concurrent.Executor;
2328
import javax.inject.Inject;
2429

2530
/** Sends tester feedback to the Tester API. */
2631
class FeedbackSender {
32+
static final String CONTENT_TYPE_JPEG = "image/jpeg";
33+
static final String CONTENT_TYPE_PNG = "image/png";
34+
35+
private static final String TAG = "FeedbackSender";
36+
private static final String FILE_EXTENSION_JPG = ".jpg";
37+
private static final String FILE_EXTENSION_JPEG = ".jpeg";
38+
private static final String FILE_EXTENSION_PNG = ".png";
39+
private static final String DEFAULT_FILENAME = "screenshot.png";
40+
41+
private final ContentResolver contentResolver;
2742
private final FirebaseAppDistributionTesterApiClient testerApiClient;
2843
@Lightweight private final Executor lightweightExecutor;
2944

3045
@Inject
3146
FeedbackSender(
47+
ContentResolver contentResolver,
3248
FirebaseAppDistributionTesterApiClient testerApiClient,
3349
@Lightweight Executor lightweightExecutor) {
50+
this.contentResolver = contentResolver;
3451
this.testerApiClient = testerApiClient;
3552
this.lightweightExecutor = lightweightExecutor;
3653
}
@@ -54,6 +71,96 @@ private Task<String> attachScreenshot(String feedbackName, @Nullable Uri screens
5471
if (screenshotUri == null) {
5572
return Tasks.forResult(feedbackName);
5673
}
57-
return testerApiClient.attachScreenshot(feedbackName, screenshotUri);
74+
if (!screenshotUri.getScheme().equals("file") && !screenshotUri.getScheme().equals("content")) {
75+
return Tasks.forException(
76+
new FirebaseAppDistributionException(
77+
String.format(
78+
"Screenshot URI '%s' was not a content or file URI. Not starting feedback.",
79+
screenshotUri),
80+
Status.UNKNOWN));
81+
}
82+
String contentType = getContentType(screenshotUri);
83+
return testerApiClient.attachScreenshot(
84+
feedbackName,
85+
screenshotUri,
86+
getScreenshotFilename(screenshotUri, contentType),
87+
contentType);
88+
}
89+
90+
private String getContentType(Uri screenshotUri) {
91+
String contentType = null;
92+
if (screenshotUri.getScheme().equals("file")) {
93+
contentType = getContentTypeForFilename(screenshotUri.getLastPathSegment());
94+
} else if (screenshotUri.getScheme().equals("content")) {
95+
contentType = contentResolver.getType(screenshotUri);
96+
}
97+
98+
if (contentType != null) {
99+
return contentType;
100+
}
101+
102+
LogWrapper.w(
103+
TAG, String.format("Could not get content type for URI %s. Assuming PNG.", screenshotUri));
104+
return CONTENT_TYPE_PNG;
105+
}
106+
107+
private String getScreenshotFilename(Uri screenshotUri, String contentType) {
108+
if (screenshotUri.getScheme().equals("file")) {
109+
return screenshotUri.getLastPathSegment();
110+
} else {
111+
return getContentFilename(screenshotUri, contentType);
112+
}
113+
}
114+
115+
private String getContentFilename(Uri contentUri, String contentType) {
116+
try (Cursor returnCursor =
117+
contentResolver.query(
118+
contentUri,
119+
new String[] {OpenableColumns.DISPLAY_NAME},
120+
/* selection= */ null,
121+
/* projectionArgs= */ null,
122+
/* sortOrder= */ null)) {
123+
if (returnCursor == null) {
124+
LogWrapper.w(
125+
TAG,
126+
String.format("Unable to get filename from URI '%s', using content type", contentUri));
127+
return getDefaultFilename(contentType);
128+
}
129+
int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
130+
returnCursor.moveToFirst();
131+
String name = returnCursor.getString(nameIndex);
132+
return name;
133+
} catch (Exception e) {
134+
LogWrapper.e(
135+
TAG,
136+
String.format("Error getting filename from URI '%s', using content type", contentUri),
137+
e);
138+
return getDefaultFilename(contentType);
139+
}
140+
}
141+
142+
private String getDefaultFilename(String contentType) {
143+
if (contentType.equals(CONTENT_TYPE_JPEG)) {
144+
return "screenshot.jpg";
145+
} else if (contentType.equals(CONTENT_TYPE_PNG)) {
146+
return "screenshot.png";
147+
} else {
148+
LogWrapper.w(
149+
TAG,
150+
String.format(
151+
"Unexpected content type '%s', using filename without extension", contentType));
152+
return "screenshot";
153+
}
154+
}
155+
156+
@Nullable
157+
private static String getContentTypeForFilename(String filename) {
158+
if (filename.endsWith(FILE_EXTENSION_JPG) || filename.endsWith(FILE_EXTENSION_JPEG)) {
159+
return CONTENT_TYPE_JPEG;
160+
} else if (filename.endsWith(FILE_EXTENSION_PNG)) {
161+
return CONTENT_TYPE_PNG;
162+
} else {
163+
return null;
164+
}
58165
}
59166
}

firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/FirebaseAppDistributionImpl.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,14 @@ private void startFeedback(
381381
@NonNull CharSequence additionalFormText,
382382
@Nullable Uri screenshotUri,
383383
FeedbackTrigger trigger) {
384+
if (!screenshotUri.getScheme().equals("content") && !screenshotUri.getScheme().equals("file")) {
385+
LogWrapper.e(
386+
TAG,
387+
String.format(
388+
"Screenshot URI %s was not a content or file URI. Not starting feedback.",
389+
screenshotUri));
390+
return;
391+
}
384392
if (!feedbackInProgress.compareAndSet(/* expect= */ false, /* update= */ true)) {
385393
LogWrapper.i(TAG, "Ignoring startFeedback() call because feedback is already in progress");
386394
return;

0 commit comments

Comments
 (0)