diff --git a/.gitignore b/.gitignore index cbaee66..874a156 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ local.properties app/release/* app/build/* +app/backup_build/* .gradle/* build/* .idea/* diff --git a/README.md b/README.md index 7108ad2..fa56baf 100644 --- a/README.md +++ b/README.md @@ -262,3 +262,48 @@ Wireless debugging can sometimes be tricky. Here are some common issues and how ```bash adb uninstall atorch.statspuzzles ``` + +## Generating and Testing a Release App Bundle + +Here's how to generate a signed Android App Bundle (AAB) for release and test it on a physical device. + +### 1. Generate a Signed App Bundle + +To create a release-ready App Bundle, you'll need a signing key. If you don't have one, you can generate one using `keytool`. Make sure you have configured your `app/build.gradle` to use your keystore for release builds. + +Once your signing configuration is set up, run the following command: + +```bash +./gradlew bundleRelease +``` + +This will generate a signed AAB file at `app/build/outputs/bundle/release/app-release.aab`. + +### 2. Test the App Bundle on Your Device + +You can't install an AAB file directly. You need to use `bundletool` to generate a set of APKs and install them on your device. + +1. **Download `bundletool`:** + Download the `bundletool` jar from the [Android Developer website](https://developer.android.com/studio/command-line/bundletool). + +2. **Connect your device:** + Make sure your device is connected via Wi-Fi debugging as described in the "Running the App on a Physical Device (Wireless Debugging)" section. + +3. **Generate APKs from the App Bundle:** + Use `bundletool` to build the APKs. You will need to provide your keystore information. + + ```bash + java -jar /path/to/bundletool.jar build-apks --bundle=app/build/outputs/bundle/release/app-release.aab --output=app.apks \ + --ks=/path/to/your/keystore.jks \ + --ks-pass=pass:YOUR_KEYSTORE_PASSWORD \ + --ks-key-alias=YOUR_KEY_ALIAS \ + --key-pass=pass:YOUR_KEY_PASSWORD + ``` + **Important:** Replace the placeholder values (`/path/to/bundletool.jar`, `/path/to/your/keystore.jks`, `YOUR_KEYSTORE_PASSWORD`, `YOUR_KEY_ALIAS`, `YOUR_KEY_PASSWORD`) with your actual information. + +4. **Install the APKs on your device:** + ```bash + java -jar /path/to/bundletool.jar install-apks --apks=app.apks + ``` + + `bundletool` will install the correct APKs for your device's architecture. You can now test the release version of your app. \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 0b9c8b8..9c95df5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,12 +1,31 @@ apply plugin: 'com.android.application' +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + android { compileSdkVersion 35 + signingConfigs { + release { + if (localPropertiesFile.exists()) { + keyAlias localProperties['signing.key.alias'] + keyPassword localProperties['signing.key.password'] + storeFile file(localProperties['signing.store.file']) + storePassword localProperties['signing.store.password'] + } + } + } + defaultConfig { applicationId "atorch.statspuzzles" minSdkVersion 19 - targetSdkVersion 34 + targetSdkVersion 35 versionCode 32 versionName "4.0" @@ -17,6 +36,10 @@ android { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + // Only apply the signing config if the local.properties file exists (it will not on github) + if (project.rootProject.file('local.properties').exists()) { + signingConfig signingConfigs.release + } } } namespace 'atorch.statspuzzles' diff --git a/app/src/androidTest/java/atorch/statspuzzles/PuzzleSelectionTest.java b/app/src/androidTest/java/atorch/statspuzzles/PuzzleSelectionTest.java index 82adc28..d274392 100644 --- a/app/src/androidTest/java/atorch/statspuzzles/PuzzleSelectionTest.java +++ b/app/src/androidTest/java/atorch/statspuzzles/PuzzleSelectionTest.java @@ -24,7 +24,7 @@ public class PuzzleSelectionTest { @Test public void mainActivityLoads() { Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); - Espresso.onView(ViewMatchers.withText(context.getString(R.string.app_name))) + Espresso.onView(ViewMatchers.withText(context.getString(R.string.button_intro))) .check(ViewAssertions.matches(ViewMatchers.isDisplayed())); } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 08ed1ef..219c156 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,7 +15,7 @@ diff --git a/app/src/main/java/atorch/statspuzzles/PuzzleSelection.java b/app/src/main/java/atorch/statspuzzles/PuzzleSelection.java index 430834a..9f22a13 100644 --- a/app/src/main/java/atorch/statspuzzles/PuzzleSelection.java +++ b/app/src/main/java/atorch/statspuzzles/PuzzleSelection.java @@ -9,6 +9,7 @@ import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; public class PuzzleSelection extends AppCompatActivity { @@ -29,6 +30,9 @@ public void startPuzzle(View view) { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_puzzle_selection); + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayShowTitleEnabled(false); updateCounter(); AppRater.app_launched(this); } diff --git a/app/src/main/java/atorch/statspuzzles/SolvePuzzle.java b/app/src/main/java/atorch/statspuzzles/SolvePuzzle.java index fb31f57..9f1a3e5 100644 --- a/app/src/main/java/atorch/statspuzzles/SolvePuzzle.java +++ b/app/src/main/java/atorch/statspuzzles/SolvePuzzle.java @@ -28,8 +28,7 @@ import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.ShareActionProvider; -import androidx.core.view.MenuItemCompat; + import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; @@ -48,7 +47,7 @@ public class SolvePuzzle extends AppCompatActivity { // Following example at https://developer.android.com/training/sharing/shareaction.html - private ShareActionProvider mShareActionProvider; + public static final String PUZZLE_INDEX = "atorch.statspuzzles.PUZZLE_INDEX"; public static final String LEVEL = "atorch.statspuzzles.LEVEL"; @@ -65,6 +64,10 @@ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_solve_puzzle); + androidx.appcompat.widget.Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayShowTitleEnabled(false); + ActionBar actionBar = getSupportActionBar(); actionBar.setHomeButtonEnabled(true); actionBar.setDisplayHomeAsUpEnabled(true); @@ -79,13 +82,7 @@ public void onCreate(Bundle savedInstanceState) { puzzlePager = findViewById(R.id.pager); puzzlePager.setAdapter(new AppSectionsPagerAdapter(this, level, res)); - puzzlePager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { - @Override - public void onPageSelected(int puzzleIndex) { - super.onPageSelected(puzzleIndex); - prepareSharePuzzle(res.getPuzzle(level, puzzleIndex)); - } - }); + puzzlePager.setCurrentItem(indexFirstUnsolvedPuzzle()); } @@ -101,35 +98,29 @@ private int indexFirstUnsolvedPuzzle() { return n - 1; // Everything solved, return last index } - private void prepareSharePuzzle(String puzzleStatement) { - String text = puzzleStatement + "\n\n" + getString(R.string.app_link); - Intent shareIntent = new Intent(); - shareIntent.setAction(Intent.ACTION_SEND); - shareIntent.putExtra(Intent.EXTRA_TEXT, text); - shareIntent.setType("text/plain"); - if (mShareActionProvider != null) { - // Should be called whenever new fragment is displayed - mShareActionProvider.setShareIntent(shareIntent); - } - } + @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.solve_puzzle, menu); - MenuItem item = menu.findItem(R.id.menu_item_share); - - // Following http://stackoverflow.com/questions/19118051/unable-to-cast-action-provider-to-share-action-provider - mShareActionProvider = (ShareActionProvider) MenuItemCompat.getActionProvider(item); - if (mShareActionProvider == null) { - // Following http://stackoverflow.com/questions/19358510/why-menuitemcompat-getactionprovider-returns-null - mShareActionProvider = new ShareActionProvider(this); - MenuItemCompat.setActionProvider(item, mShareActionProvider); - } - prepareSharePuzzle(res.getPuzzle(level, puzzlePager.getCurrentItem())); return true; // Return true to display menu } + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.menu_item_share) { + String puzzleStatement = res.getPuzzle(level, puzzlePager.getCurrentItem()); + String text = puzzleStatement + "\n\n" + getString(R.string.app_link); + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.putExtra(Intent.EXTRA_TEXT, text); + shareIntent.setType("text/plain"); + startActivity(Intent.createChooser(shareIntent, null)); + return true; + } + return super.onOptionsItemSelected(item); + } + private static class Res { // Level -1 is the introduction / how to. diff --git a/app/src/main/res/layout/activity_puzzle_selection.xml b/app/src/main/res/layout/activity_puzzle_selection.xml index d64f372..764c80b 100644 --- a/app/src/main/res/layout/activity_puzzle_selection.xml +++ b/app/src/main/res/layout/activity_puzzle_selection.xml @@ -6,11 +6,29 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + + + + + + @@ -30,6 +48,7 @@ android:paddingTop="@dimen/activity_vertical_margin_half" >