Skip to content

Commit e68df63

Browse files
committed
initial commit
0 parents  commit e68df63

21 files changed

Lines changed: 993 additions & 0 deletions

File tree

‎.editorconfig‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
end_of_line = lf
6+
insert_final_newline = true
7+
8+
[*.{java,gradle.kts}]
9+
indent_style = space
10+
indent_size = 4
11+
12+
[*.{json,md,xml,yml}]
13+
indent_style = space
14+
indent_size = 2
15+

‎.github/workflows/checks.yml‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: Checks
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
9+
permissions:
10+
contents: read
11+
packages: read
12+
13+
concurrency:
14+
group: checks-${{ github.workflow }}-${{ github.ref }}
15+
cancel-in-progress: true
16+
17+
jobs:
18+
checks:
19+
runs-on: ubuntu-latest
20+
timeout-minutes: 20
21+
22+
env:
23+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
24+
25+
steps:
26+
- name: Check out repository
27+
uses: actions/checkout@v7
28+
29+
- name: Set up Java
30+
uses: actions/setup-java@v5
31+
with:
32+
distribution: temurin
33+
java-version: "17"
34+
35+
- name: Set up Gradle
36+
uses: gradle/actions/setup-gradle@v6
37+
38+
- name: Test, lint, and build
39+
run: ./gradlew --no-daemon testDebugUnitTest lintDebug assembleDebug

‎.gitignore‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Android Studio and Gradle local state
2+
.gradle/
3+
.kotlin/
4+
.idea/
5+
.vscode/
6+
.fleet/
7+
.settings/
8+
.project
9+
.classpath
10+
*.iml
11+
local.properties
12+
13+
# Build output
14+
build/
15+
app/build/
16+
/app/release/
17+
out/
18+
captures/
19+
.externalNativeBuild/
20+
.cxx/
21+
*.apk
22+
*.aab
23+
*.ap_
24+
*.dex
25+
26+
# Local environment, credentials, and signing files
27+
.env
28+
.env.*
29+
!.env.example
30+
secrets.properties
31+
keystore.properties
32+
*.jks
33+
*.keystore
34+
*.p12
35+
*.pem
36+
*.key
37+
google-services.json
38+
service-account*.json
39+
40+
# Operating system files
41+
.DS_Store
42+
Thumbs.db
43+
*.swp
44+
*.swo
45+
*~
46+
*.log

‎README.md‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# featurevisor-example-android
2+
3+
A small Android application showing how to use the [Featurevisor Java SDK](https://github.com/featurevisor/featurevisor-java) from Java.
4+
5+
The application downloads a Featurevisor datafile, creates an SDK instance, and evaluates the `mobile_experience` feature as a flag, variation, and string variable.
6+
7+
## Requirements
8+
9+
Install Android Studio with Android SDK 37 and JDK 17.
10+
11+
The Featurevisor Java SDK is published through GitHub Packages. Create a GitHub personal access token with the `read:packages` scope, then add these values to `~/.gradle/gradle.properties`:
12+
13+
```properties
14+
gpr.user=YOUR_GITHUB_USERNAME
15+
gpr.key=YOUR_GITHUB_TOKEN
16+
```
17+
18+
Keep these credentials outside the repository. For GitHub Actions, grant the example repository access to the `featurevisor-java` package.
19+
20+
## Run the application
21+
22+
1. Open the repository in Android Studio.
23+
2. Allow Gradle to sync and resolve the Featurevisor dependency.
24+
3. Select an Android emulator or connected device.
25+
4. Run the `app` configuration.
26+
27+
## Featurevisor usage
28+
29+
The application fetches this production datafile:
30+
31+
```text
32+
https://featurevisor-example-cloudflare.pages.dev/production/featurevisor-mobile.json
33+
```
34+
35+
The important SDK usage is in `MainActivity.java`:
36+
37+
```java
38+
DatafileContent datafile = DatafileContent.fromJson(datafileJson);
39+
Featurevisor f = Featurevisor.createFeaturevisor(
40+
new Featurevisor.FeaturevisorOptions().datafile(datafile)
41+
);
42+
43+
Map<String, Object> context = new HashMap<>();
44+
context.put("userId", "mobile-user");
45+
context.put("country", "nl");
46+
47+
boolean enabled = f.isEnabled("mobile_experience", context);
48+
String variation = f.getVariation("mobile_experience", context);
49+
String message = f.getVariableString(
50+
"mobile_experience",
51+
"welcome_message",
52+
context
53+
);
54+
```
55+
56+
## Checks
57+
58+
Run the unit tests, Android lint, and a debug build with:
59+
60+
```sh
61+
./gradlew testDebugUnitTest lintDebug assembleDebug
62+
```
63+
64+
Learn more in the [Featurevisor Java SDK documentation](https://featurevisor.com/docs/sdks/java/).

‎app/build.gradle.kts‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
plugins {
2+
id("com.android.application")
3+
}
4+
5+
android {
6+
namespace = "com.featurevisor.example.android"
7+
compileSdk = 37
8+
9+
defaultConfig {
10+
applicationId = "com.featurevisor.example.android"
11+
minSdk = 23
12+
targetSdk = 37
13+
versionCode = 1
14+
versionName = "1.0"
15+
}
16+
17+
compileOptions {
18+
isCoreLibraryDesugaringEnabled = true
19+
sourceCompatibility = JavaVersion.VERSION_17
20+
targetCompatibility = JavaVersion.VERSION_17
21+
}
22+
}
23+
24+
dependencies {
25+
implementation("com.featurevisor:featurevisor-java:3.0.0")
26+
27+
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
28+
29+
testImplementation("junit:junit:4.13.2")
30+
}
31+

‎app/src/main/AndroidManifest.xml‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
3+
<uses-permission android:name="android.permission.INTERNET" />
4+
5+
<application
6+
android:allowBackup="true"
7+
android:label="@string/app_name"
8+
android:supportsRtl="true"
9+
android:theme="@style/Theme.FeaturevisorExample">
10+
<activity
11+
android:name=".MainActivity"
12+
android:exported="true">
13+
<intent-filter>
14+
<action android:name="android.intent.action.MAIN" />
15+
16+
<category android:name="android.intent.category.LAUNCHER" />
17+
</intent-filter>
18+
</activity>
19+
</application>
20+
</manifest>
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
package com.featurevisor.example.android;
2+
3+
import android.app.Activity;
4+
import android.os.Bundle;
5+
import android.view.View;
6+
import android.widget.TextView;
7+
8+
import com.featurevisor.sdk.DatafileContent;
9+
import com.featurevisor.sdk.Featurevisor;
10+
11+
import java.io.BufferedReader;
12+
import java.io.IOException;
13+
import java.io.InputStreamReader;
14+
import java.net.HttpURLConnection;
15+
import java.net.URL;
16+
import java.nio.charset.StandardCharsets;
17+
import java.util.HashMap;
18+
import java.util.Map;
19+
import java.util.concurrent.ExecutorService;
20+
import java.util.concurrent.Executors;
21+
22+
public final class MainActivity extends Activity {
23+
private static final String DATAFILE_URL =
24+
"https://featurevisor-example-cloudflare.pages.dev/production/featurevisor-mobile.json";
25+
26+
private final ExecutorService executor = Executors.newSingleThreadExecutor();
27+
28+
private Featurevisor f;
29+
private View loadingView;
30+
private View errorView;
31+
private View resultsView;
32+
private TextView errorMessage;
33+
private TextView flagValue;
34+
private TextView variationValue;
35+
private TextView variableValue;
36+
37+
@Override
38+
protected void onCreate(Bundle savedInstanceState) {
39+
super.onCreate(savedInstanceState);
40+
setContentView(R.layout.activity_main);
41+
42+
loadingView = findViewById(R.id.loading_view);
43+
errorView = findViewById(R.id.error_view);
44+
resultsView = findViewById(R.id.results_view);
45+
errorMessage = findViewById(R.id.error_message);
46+
flagValue = findViewById(R.id.flag_value);
47+
variationValue = findViewById(R.id.variation_value);
48+
variableValue = findViewById(R.id.variable_value);
49+
50+
findViewById(R.id.retry_button).setOnClickListener(view -> loadDatafile());
51+
52+
loadDatafile();
53+
}
54+
55+
private void loadDatafile() {
56+
showLoading();
57+
58+
executor.execute(() -> {
59+
try {
60+
String datafileJson = fetchDatafile();
61+
DatafileContent datafile = DatafileContent.fromJson(datafileJson);
62+
Featurevisor loadedF = Featurevisor.createFeaturevisor(
63+
new Featurevisor.FeaturevisorOptions().datafile(datafile)
64+
);
65+
66+
Map<String, Object> context = new HashMap<>();
67+
context.put("userId", "mobile-user");
68+
context.put("country", "nl");
69+
70+
boolean enabled = loadedF.isEnabled("mobile_experience", context);
71+
String variation = loadedF.getVariation("mobile_experience", context);
72+
String welcomeMessage = loadedF.getVariableString(
73+
"mobile_experience",
74+
"welcome_message",
75+
context
76+
);
77+
78+
runOnUiThread(() -> showResults(
79+
loadedF,
80+
enabled,
81+
variation,
82+
welcomeMessage
83+
));
84+
} catch (Exception exception) {
85+
runOnUiThread(() -> showError(exception.getMessage()));
86+
}
87+
});
88+
}
89+
90+
private String fetchDatafile() throws IOException {
91+
HttpURLConnection connection = (HttpURLConnection) new URL(DATAFILE_URL).openConnection();
92+
connection.setConnectTimeout(10_000);
93+
connection.setReadTimeout(10_000);
94+
connection.setRequestProperty("Accept", "application/json");
95+
96+
try {
97+
int statusCode = connection.getResponseCode();
98+
99+
if (statusCode < 200 || statusCode >= 300) {
100+
throw new IOException("Datafile request failed with HTTP " + statusCode + ".");
101+
}
102+
103+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(
104+
connection.getInputStream(),
105+
StandardCharsets.UTF_8
106+
))) {
107+
StringBuilder content = new StringBuilder();
108+
String line;
109+
110+
while ((line = reader.readLine()) != null) {
111+
content.append(line);
112+
}
113+
114+
return content.toString();
115+
}
116+
} finally {
117+
connection.disconnect();
118+
}
119+
}
120+
121+
private void showLoading() {
122+
loadingView.setVisibility(View.VISIBLE);
123+
errorView.setVisibility(View.GONE);
124+
resultsView.setVisibility(View.GONE);
125+
}
126+
127+
private void showResults(
128+
Featurevisor loadedF,
129+
boolean enabled,
130+
String variation,
131+
String welcomeMessage
132+
) {
133+
if (isFinishing() || isDestroyed()) {
134+
loadedF.close();
135+
return;
136+
}
137+
138+
if (f != null) {
139+
f.close();
140+
}
141+
142+
f = loadedF;
143+
flagValue.setText(enabled ? R.string.enabled : R.string.disabled);
144+
variationValue.setText(valueOrFallback(variation));
145+
variableValue.setText(valueOrFallback(welcomeMessage));
146+
loadingView.setVisibility(View.GONE);
147+
errorView.setVisibility(View.GONE);
148+
resultsView.setVisibility(View.VISIBLE);
149+
}
150+
151+
private void showError(String message) {
152+
if (isFinishing() || isDestroyed()) {
153+
return;
154+
}
155+
156+
loadingView.setVisibility(View.GONE);
157+
errorView.setVisibility(View.VISIBLE);
158+
resultsView.setVisibility(View.GONE);
159+
errorMessage.setText(message != null ? message : getString(R.string.unknown_error));
160+
}
161+
162+
private String valueOrFallback(String value) {
163+
return value != null ? value : getString(R.string.no_value);
164+
}
165+
166+
@Override
167+
protected void onDestroy() {
168+
executor.shutdownNow();
169+
170+
if (f != null) {
171+
f.close();
172+
}
173+
174+
super.onDestroy();
175+
}
176+
}

0 commit comments

Comments
 (0)