Make the Rust client the sole app

This commit is contained in:
iris committed 2026-09-11 01:18:24 -04:00
1 parent a8602c1626
commit d8bb1699a8
230 files changed
+762 -27300

No files matched your search

+63
View File
@@ -0,0 +1,63 @@
plugins {
id("com.android.application")
}
// build-apk.sh places the Rust cdylib in src/main/jniLibs before Gradle runs.
def benchBuild = System.getenv("AI_APP_BENCH") == "1"
android {
namespace = "dev.iris.android.demo"
compileSdk = 37
defaultConfig {
applicationId = "com.example.aiapp"
// 29, not 26: `iris::android::view`'s touch handler dates each
// sample with `MotionEvent.getEventTimeNanos` and
// `getHistoricalEventTimeNanos`, both API 29, and a missing JNI
// method there is a hard crash on the first touch rather than a
// degraded fling. Raised deliberately rather than guarded at
// runtime: nothing this app is built for runs below 29, and an
// untested fallback path is its own defect. `build-apk.sh`'s
// `cargo ndk -P` is kept at the same number.
minSdk = 29
// targetSdk 35+ supplies real IME overlap under enforced edge-to-edge.
targetSdk = 37
versionCode = 1
versionName = "1.0"
manifestPlaceholders = [appLabel: benchBuild ? "AI Sessions bench" : "AI Sessions"]
}
// The signing key is machine-local; build-apk.sh creates and supplies it.
def keystore = System.getenv("AI_APP_KEYSTORE")
signingConfigs {
if (keystore != null) {
release {
storeFile = file(keystore)
storePassword = System.getenv("AI_APP_KEYSTORE_PASSWORD")
keyAlias = "ai-app"
keyPassword = storePassword
}
}
}
buildTypes {
debug {
if (benchBuild) {
applicationIdSuffix ".bench"
}
}
release {
if (benchBuild) {
applicationIdSuffix ".bench"
}
if (keystore != null) {
signingConfig = signingConfigs.release
}
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The transcript client talks to ai-server. -->
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:label="${appLabel}"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:exported="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Enrollment links minted by ai-server and Dev Updater. -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="aiapp" android:host="enroll" />
</intent-filter>
<meta-data android:name="android.app.lib_name" android:value="ai_app" />
</activity>
<!-- Read-only recent logs for Dev Updater. The authority follows
applicationId so normal and benchmark builds stay separate. -->
<provider
android:name=".DevLogProvider"
android:authorities="${applicationId}.devlog"
android:exported="true"
android:readPermission="dev.updater.permission.READ_DEVLOG" />
</application>
</manifest>
@@ -0,0 +1,125 @@
package dev.iris.android.demo;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
/** Read-only Dev Updater log provider; its URI and column schema are an external contract. */
public final class DevLogProvider extends ContentProvider {
static {
// A provider can start the process without creating MainActivity.
System.loadLibrary("ai_app");
}
private static final int FIELDS_PER_LINE = 5;
private static final String[] LINE_COLUMNS = {"seq", "t_ms", "level", "target", "message"};
private static final String[] STATUS_COLUMNS = {"held", "dropped", "newest_seq"};
private static final int LINES = 1;
private static final int STATUS = 2;
private UriMatcher matcher;
private static native String[] nativeLinesSince(long since);
private static native String[] nativeStatus();
// The provider may be the process's only component, so it must supply
// the files directory normally initialized by MainActivity.
private static native void nativeReady(String authority, String filesDir);
@Override
public boolean onCreate() {
String authority = getContext().getPackageName() + ".devlog";
matcher = new UriMatcher(UriMatcher.NO_MATCH);
matcher.addURI(authority, "lines", LINES);
matcher.addURI(authority, "status", STATUS);
nativeReady(authority, getContext().getFilesDir().getAbsolutePath());
return true;
}
@Override
public Cursor query(
Uri uri,
String[] projection,
String selection,
String[] selectionArgs,
String sortOrder) {
switch (matcher.match(uri)) {
case LINES:
return lines(sinceOf(uri));
case STATUS:
return status();
default:
return null;
}
}
private static long sinceOf(Uri uri) {
String since = uri.getQueryParameter("since");
if (since == null) {
return 0;
}
try {
return Long.parseLong(since);
} catch (NumberFormatException ignored) {
return 0;
}
}
private static Cursor lines(long since) {
String[] fields = nativeLinesSince(since);
if (fields == null) {
return null;
}
MatrixCursor cursor = new MatrixCursor(LINE_COLUMNS, fields.length / FIELDS_PER_LINE);
for (int at = 0; at + FIELDS_PER_LINE <= fields.length; at += FIELDS_PER_LINE) {
cursor.addRow(
new Object[] {
Long.parseLong(fields[at]),
Long.parseLong(fields[at + 1]),
fields[at + 2],
fields[at + 3],
fields[at + 4],
});
}
return cursor;
}
private static Cursor status() {
String[] fields = nativeStatus();
if (fields == null || fields.length != STATUS_COLUMNS.length) {
return null;
}
MatrixCursor cursor = new MatrixCursor(STATUS_COLUMNS, 1);
cursor.addRow(
new Object[] {
Long.parseLong(fields[0]), Long.parseLong(fields[1]), Long.parseLong(fields[2]),
});
return cursor;
}
@Override
public String getType(Uri uri) {
return null;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
throw new UnsupportedOperationException("this app's log is read-only");
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
throw new UnsupportedOperationException("this app's log is read-only");
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
throw new UnsupportedOperationException("this app's log is read-only");
}
}
@@ -0,0 +1,60 @@
package dev.iris.android.demo;
import android.app.Activity;
import android.content.Context;
import android.view.Gravity;
import android.widget.ScrollView;
import android.widget.TextView;
import org.linebender.android.rustview.RustView;
/**
* android-view's abstract base plus the two native methods it has no hook
* for: window insets and unregistering this view's entry in
* iris::android::insets's side table. See iris/src/android/insets.rs's doc
* comment for why those could not ride along on an existing android-view
* callback the way the back gesture does.
*/
public final class IrisView extends RustView {
@Override
protected native long newViewPeer(Context context);
native void applyWindowInsetsNative(
long peer, int left, int top, int right, int bottom, int imeBottom, int imeVisible);
native void unregisterInsetsNative(long peer);
public IrisView(Context context) {
super(context);
}
void applyWindowInsets(
int left, int top, int right, int bottom, int imeBottom, int imeVisible) {
applyWindowInsetsNative(mViewPeer, left, top, right, bottom, imeBottom, imeVisible);
}
@Override
protected void onDetachedFromWindow() {
unregisterInsetsNative(mViewPeer);
super.onDetachedFromWindow();
}
// This path must not depend on the renderer that failed to initialize.
void showRendererError(String report) {
Context context = getContext();
if (!(context instanceof Activity)) {
return;
}
Activity activity = (Activity) context;
TextView text = new TextView(activity);
text.setText(report);
text.setTextIsSelectable(true);
text.setGravity(Gravity.TOP | Gravity.START);
int pad = (int) (16 * activity.getResources().getDisplayMetrics().density);
text.setPadding(pad, pad, pad, pad);
ScrollView scroll = new ScrollView(activity);
scroll.addView(text);
activity.setContentView(scroll);
}
}
@@ -0,0 +1,104 @@
package dev.iris.android.demo;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.view.WindowInsets;
import android.view.WindowInsetsAnimation;
import android.widget.FrameLayout;
import java.util.List;
public final class MainActivity extends Activity {
static {
System.loadLibrary("ai_app");
}
private static native void nativeSetFilesDir(String path);
private static native void nativeEnroll(String uri);
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
nativeSetFilesDir(getFilesDir().getAbsolutePath());
handleEnrollmentIntent(getIntent());
IrisView view = new IrisView(this);
view.setLayoutParams(new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
view.setFocusable(true);
view.setFocusableInTouchMode(true);
FrameLayout layout = new FrameLayout(this);
layout.addView(view);
setContentView(layout);
view.requestFocus();
// Edge-to-edge makes IME-only changes produce fresh inset dispatches.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
getWindow().setDecorFitsSystemWindows(false);
}
// Static dispatch supplies settled insets; the animation callback
// supplies intermediate IME heights. An interrupted animation may
// omit its final progress frame, so onEnd re-reads the root insets.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
view.setWindowInsetsAnimationCallback(new WindowInsetsAnimation.Callback(
WindowInsetsAnimation.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
@Override
public WindowInsets onProgress(
WindowInsets insets, List<WindowInsetsAnimation> running) {
sendInsets(view, insets);
return insets;
}
@Override
public void onEnd(WindowInsetsAnimation animation) {
WindowInsets settled = view.getRootWindowInsets();
if (settled != null) {
sendInsets(view, settled);
}
}
});
}
view.setOnApplyWindowInsetsListener((v, insets) -> {
sendInsets((IrisView) v, insets);
return insets;
});
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
// Keep getIntent() consistent with the enrollment being handled.
setIntent(intent);
handleEnrollmentIntent(intent);
}
private static void handleEnrollmentIntent(Intent intent) {
if (intent == null) {
return;
}
Uri data = intent.getData();
if (data != null) {
nativeEnroll(data.toString());
}
}
private static void sendInsets(IrisView view, WindowInsets insets) {
int left = insets.getSystemWindowInsetLeft();
int top = insets.getSystemWindowInsetTop();
int right = insets.getSystemWindowInsetRight();
int bottom = insets.getSystemWindowInsetBottom();
// Visibility and height disagree during IME animation, so neither
// can be inferred from the other.
int imeBottom = 0;
int imeVisible = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
imeBottom = insets.getInsets(WindowInsets.Type.ime()).bottom;
imeVisible = insets.isVisible(WindowInsets.Type.ime()) ? 1 : 0;
}
view.applyWindowInsets(left, top, right, bottom, imeBottom, imeVisible);
}
}
@@ -0,0 +1,153 @@
package org.linebender.android.rustview;
import android.os.Bundle;
import android.os.Handler;
import android.view.KeyEvent;
import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.CorrectionInfo;
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputContentInfo;
class RustInputConnection implements InputConnection {
private final RustView mView;
RustInputConnection(RustView view) {
mView = view;
}
private long getViewPeer() {
return mView.mViewPeer;
}
@Override
public CharSequence getTextBeforeCursor(int n, int flags) {
return mView.getTextBeforeCursorNative(getViewPeer(), n);
}
@Override
public CharSequence getTextAfterCursor(int n, int flags) {
return mView.getTextAfterCursorNative(getViewPeer(), n);
}
@Override
public CharSequence getSelectedText(int flags) {
return mView.getSelectedTextNative(getViewPeer());
}
@Override
public int getCursorCapsMode(int reqModes) {
return mView.getCursorCapsModeNative(getViewPeer(), reqModes);
}
@Override
public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
return null;
}
@Override
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
return mView.deleteSurroundingTextNative(getViewPeer(), beforeLength, afterLength);
}
@Override
public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) {
return mView.deleteSurroundingTextInCodePointsNative(getViewPeer(), beforeLength, afterLength);
}
@Override
public boolean setComposingText(CharSequence text, int newCursorPosition) {
return mView.setComposingTextNative(getViewPeer(), text.toString(), newCursorPosition);
}
@Override
public boolean setComposingRegion(int start, int end) {
return mView.setComposingRegionNative(getViewPeer(), start, end);
}
@Override
public boolean finishComposingText() {
return mView.finishComposingTextNative(getViewPeer());
}
@Override
public boolean commitText(CharSequence text, int newCursorPosition) {
return mView.commitTextNative(getViewPeer(), text.toString(), newCursorPosition);
}
@Override
public boolean commitCompletion(CompletionInfo text) {
return false;
}
@Override
public boolean commitCorrection(CorrectionInfo correctionInfo) {
return false;
}
@Override
public boolean setSelection(int start, int end) {
return mView.setSelectionNative(getViewPeer(), start, end);
}
@Override
public boolean performEditorAction(int editorAction) {
return mView.performEditorActionNative(getViewPeer(), editorAction);
}
@Override
public boolean performContextMenuAction(int id) {
return mView.performContextMenuActionNative(getViewPeer(), id);
}
@Override
public boolean beginBatchEdit() {
return mView.beginBatchEditNative(getViewPeer());
}
@Override
public boolean endBatchEdit() {
return mView.endBatchEditNative(getViewPeer());
}
@Override
public boolean sendKeyEvent(KeyEvent event) {
return mView.inputConnectionSendKeyEventNative(getViewPeer(), event);
}
@Override
public boolean clearMetaKeyStates(int states) {
return mView.inputConnectionClearMetaKeyStatesNative(getViewPeer(), states);
}
@Override
public boolean reportFullscreenMode(boolean enabled) {
return mView.inputConnectionReportFullscreenModeNative(getViewPeer(), enabled);
}
@Override
public boolean performPrivateCommand(String action, Bundle data) {
return false;
}
@Override
public boolean requestCursorUpdates(int cursorUpdateMode) {
return mView.requestCursorUpdatesNative(getViewPeer(), cursorUpdateMode);
}
@Override
public Handler getHandler() {
return null;
}
@Override
public void closeConnection() {
mView.closeInputConnectionNative(getViewPeer());
}
@Override
public boolean commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts) {
return false;
}
}
@@ -0,0 +1,287 @@
package org.linebender.android.rustview;
import android.content.Context;
import android.graphics.Rect;
import android.os.Bundle;
import android.view.Choreographer;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeProvider;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
public abstract class RustView extends SurfaceView
implements SurfaceHolder.Callback, Choreographer.FrameCallback {
// Vendored from android-view bec6c62. The only local change is `protected`,
// allowing IrisView to forward insets through this native peer.
protected final long mViewPeer;
final InputMethodManager mInputMethodManager;
protected abstract long newViewPeer(Context context);
public RustView(Context context) {
super(context);
mViewPeer = newViewPeer(context);
getHolder().addCallback(this);
mInputMethodManager =
(InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
}
private native int[] onMeasureNative(long peer, int widthSpec, int heightSpec);
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
int[] result = onMeasureNative(mViewPeer, widthSpec, heightSpec);
if (result != null) {
setMeasuredDimension(result[0], result[1]);
} else {
super.onMeasure(widthSpec, heightSpec);
}
}
private native void onLayoutNative(
long peer, boolean changed, int left, int top, int right, int bottom);
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
onLayoutNative(mViewPeer, changed, left, top, right, bottom);
super.onLayout(changed, left, top, right, bottom);
}
private native void onSizeChangedNative(long peer, int w, int h, int oldw, int oldh);
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
onSizeChangedNative(mViewPeer, w, h, oldw, oldh);
super.onSizeChanged(w, h, oldw, oldh);
}
private native boolean onKeyDownNative(long peer, int keyCode, KeyEvent event);
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
return onKeyDownNative(mViewPeer, keyCode, event) || super.onKeyDown(keyCode, event);
}
private native boolean onKeyUpNative(long peer, int keyCode, KeyEvent event);
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
return onKeyUpNative(mViewPeer, keyCode, event) || super.onKeyUp(keyCode, event);
}
private native boolean onTrackballEventNative(long peer, MotionEvent event);
@Override
public boolean onTrackballEvent(MotionEvent event) {
return onTrackballEventNative(mViewPeer, event) || super.onTrackballEvent(event);
}
private native boolean onTouchEventNative(long peer, MotionEvent event);
@Override
public boolean onTouchEvent(MotionEvent event) {
return onTouchEventNative(mViewPeer, event) || super.onTouchEvent(event);
}
private native boolean onGenericMotionEventNative(long peer, MotionEvent event);
@Override
public boolean onGenericMotionEvent(MotionEvent event) {
return onGenericMotionEventNative(mViewPeer, event) || super.onGenericMotionEvent(event);
}
private native boolean onHoverEventNative(long peer, MotionEvent event);
@Override
public boolean onHoverEvent(MotionEvent event) {
return onHoverEventNative(mViewPeer, event) || super.onHoverEvent(event);
}
private native void onFocusChangedNative(
long peer, boolean gainFocus, int direction, Rect previouslyFocusedRect);
@Override
protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
onFocusChangedNative(mViewPeer, gainFocus, direction, previouslyFocusedRect);
}
private native void onWindowFocusChangedNative(long peer, boolean hasWindowFocus);
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
onWindowFocusChangedNative(mViewPeer, hasWindowFocus);
}
private native void onAttachedToWindowNative(long peer);
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
onAttachedToWindowNative(mViewPeer);
}
private native void onDetachedFromWindowNative(long peer);
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
onDetachedFromWindowNative(mViewPeer);
}
private native void onWindowVisibilityChangedNative(long peer, int visibility);
@Override
protected void onWindowVisibilityChanged(int visibility) {
super.onWindowVisibilityChanged(visibility);
onWindowVisibilityChangedNative(mViewPeer, visibility);
}
private native void surfaceCreatedNative(long peer, SurfaceHolder holder);
@Override
public void surfaceCreated(SurfaceHolder holder) {
surfaceCreatedNative(mViewPeer, holder);
}
private native void surfaceChangedNative(
long peer, SurfaceHolder holder, int format, int width, int height);
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
surfaceChangedNative(mViewPeer, holder, format, width, height);
}
private native void surfaceDestroyedNative(long peer, SurfaceHolder holder);
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
surfaceDestroyedNative(mViewPeer, holder);
}
void postFrameCallback() {
Choreographer c = Choreographer.getInstance();
c.removeFrameCallback(this);
c.postFrameCallback(this);
}
void removeFrameCallback() {
Choreographer.getInstance().removeFrameCallback(this);
}
private native void doFrameNative(long peer, long frameTimeNanos);
@Override
public void doFrame(long frameTimeNanos) {
doFrameNative(mViewPeer, frameTimeNanos);
}
private native void delayedCallbackNative(long peer);
private final Runnable mDelayedCallback =
new Runnable() {
@Override
public void run() {
delayedCallbackNative(mViewPeer);
}
};
boolean postDelayed(long delayMillis) {
return postDelayed(mDelayedCallback, delayMillis);
}
boolean removeDelayedCallbacks() {
return removeCallbacks(mDelayedCallback);
}
private native boolean hasAccessibilityNodeProviderNative(long peer);
private native AccessibilityNodeInfo createAccessibilityNodeInfoNative(
long peer, int virtualViewId);
private native AccessibilityNodeInfo accessibilityFindFocusNative(long peer, int virtualViewId);
private native boolean performAccessibilityActionNative(
long peer, int virtualViewId, int action, Bundle arguments);
@Override
public AccessibilityNodeProvider getAccessibilityNodeProvider() {
if (!hasAccessibilityNodeProviderNative(mViewPeer)) {
return super.getAccessibilityNodeProvider();
}
return new AccessibilityNodeProvider() {
@Override
public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
return createAccessibilityNodeInfoNative(mViewPeer, virtualViewId);
}
@Override
public AccessibilityNodeInfo findFocus(int focusType) {
return accessibilityFindFocusNative(mViewPeer, focusType);
}
@Override
public boolean performAction(int virtualViewId, int action, Bundle arguments) {
return performAccessibilityActionNative(
mViewPeer, virtualViewId, action, arguments);
}
};
}
private native boolean onCreateInputConnectionNative(long peer, EditorInfo outAttrs);
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
if (!onCreateInputConnectionNative(mViewPeer, outAttrs)) {
return null;
}
return new RustInputConnection(this);
}
native String getTextBeforeCursorNative(long peer, int n);
native String getTextAfterCursorNative(long peer, int n);
native String getSelectedTextNative(long peer);
native int getCursorCapsModeNative(long peer, int reqModes);
native boolean deleteSurroundingTextNative(long peer, int beforeLength, int afterLength);
native boolean deleteSurroundingTextInCodePointsNative(
long peer, int beforeLength, int afterLength);
native boolean setComposingTextNative(long peer, String text, int newCursorPosition);
native boolean setComposingRegionNative(long peer, int start, int end);
native boolean finishComposingTextNative(long peer);
native boolean commitTextNative(long peer, String text, int newCursorPosition);
native boolean setSelectionNative(long peer, int start, int end);
native boolean performEditorActionNative(long peer, int editorAction);
native boolean performContextMenuActionNative(long peer, int id);
native boolean beginBatchEditNative(long peer);
native boolean endBatchEditNative(long peer);
native boolean inputConnectionSendKeyEventNative(long peer, KeyEvent event);
native boolean inputConnectionClearMetaKeyStatesNative(long peer, int states);
native boolean inputConnectionReportFullscreenModeNative(long peer, boolean enabled);
native boolean requestCursorUpdatesNative(long peer, int cursorUpdateMode);
native void closeInputConnectionNative(long peer);
}
+3
View File
@@ -0,0 +1,3 @@
plugins {
id("com.android.application") version "9.4.0" apply false
}
+15
View File
@@ -0,0 +1,15 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
rootProject.name = "iris-android-demo"
include(":app")