Merge pull request #11213 from farmerbb/play_feature_delivery

Implement support for Play Store builds using Play Feature Delivery (Java/Gradle)
This commit is contained in:
Autechre 2020-08-26 21:22:51 +02:00 committed by GitHub
commit 1898a4bec5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 516 additions and 4 deletions

View File

@ -1,5 +1,14 @@
package com.retroarch.browser.retroactivity;
import com.google.android.play.core.splitinstall.SplitInstallManager;
import com.google.android.play.core.splitinstall.SplitInstallManagerFactory;
import com.google.android.play.core.splitinstall.SplitInstallRequest;
import com.google.android.play.core.splitinstall.SplitInstallSessionState;
import com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener;
import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus;
import com.google.android.play.core.tasks.OnFailureListener;
import com.google.android.play.core.tasks.OnSuccessListener;
import com.retroarch.BuildConfig;
import com.retroarch.browser.preferences.util.UserPreferences;
import android.annotation.TargetApi;
import android.app.NativeActivity;
@ -10,6 +19,8 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.media.AudioAttributes;
import android.os.Bundle;
import android.system.Os;
import android.view.InputDevice;
import android.view.Surface;
import android.view.WindowManager;
@ -20,7 +31,14 @@ import android.os.PowerManager;
import android.os.Vibrator;
import android.os.VibrationEffect;
import android.util.Log;
import java.lang.Math;
import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.Locale;
@ -40,9 +58,60 @@ public class RetroActivityCommon extends NativeActivity
public static int FRONTEND_ORIENTATION_270 = 3;
public static int RETRO_RUMBLE_STRONG = 0;
public static int RETRO_RUMBLE_WEAK = 1;
public static int INSTALL_STATUS_DOWNLOADING = 0;
public static int INSTALL_STATUS_INSTALLING = 1;
public static int INSTALL_STATUS_INSTALLED = 2;
public static int INSTALL_STATUS_FAILED = 3;
public boolean sustainedPerformanceMode = true;
public int screenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
private final SplitInstallStateUpdatedListener listener = new SplitInstallStateUpdatedListener() {
@Override
public void onStateUpdate(SplitInstallSessionState state) {
List<String> moduleNames = state.moduleNames();
String[] coreNames = new String[moduleNames.size()];
for(int i = 0; i < moduleNames.size(); i++) {
coreNames[i] = unsanitizeCoreName(moduleNames.get(i));
}
switch(state.status()) {
case SplitInstallSessionStatus.DOWNLOADING:
coreInstallStatusChanged(coreNames, INSTALL_STATUS_DOWNLOADING, state.bytesDownloaded(), state.totalBytesToDownload());
break;
case SplitInstallSessionStatus.INSTALLING:
coreInstallStatusChanged(coreNames, INSTALL_STATUS_INSTALLING, state.bytesDownloaded(), state.totalBytesToDownload());
break;
case SplitInstallSessionStatus.INSTALLED:
updateSymlinks();
coreInstallStatusChanged(coreNames, INSTALL_STATUS_INSTALLED, state.bytesDownloaded(), state.totalBytesToDownload());
break;
case SplitInstallSessionStatus.FAILED:
coreInstallStatusChanged(coreNames, INSTALL_STATUS_FAILED, state.bytesDownloaded(), state.totalBytesToDownload());
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
updateSymlinks();
SplitInstallManager manager = SplitInstallManagerFactory.create(this);
manager.registerListener(listener);
super.onCreate(savedInstanceState);
}
@Override
protected void onDestroy() {
SplitInstallManager manager = SplitInstallManagerFactory.create(this);
manager.unregisterListener(listener);
super.onDestroy();
}
public void doVibrate(int id, int effect, int strength, int oneShot)
{
Vibrator vibrator = null;
@ -297,4 +366,240 @@ public class RetroActivityCommon extends NativeActivity
Log.i("RetroActivity", "hasOldOrientation? " + hasOldOrientation + " newOrientation: " + newConfig.orientation + " oldOrientation: " + oldOrientation);
}
/**
* Checks if this version of RetroArch is a Play Store build.
*
* @return true if this is a Play Store build, false otherwise
*/
public boolean isPlayStoreBuild() {
Log.i("RetroActivity", "isPlayStoreBuild: " + BuildConfig.PLAY_STORE_BUILD);
return BuildConfig.PLAY_STORE_BUILD;
}
/**
* Gets the list of available cores that can be downloaded as Dynamic Feature Modules.
*
* @return the list of available cores
*/
public String[] getAvailableCores() {
int id = getResources().getIdentifier("module_names_" + sanitizeCoreName(Build.CPU_ABI), "array", getPackageName());
String[] returnVal = getResources().getStringArray(id);
Log.i("RetroActivity", "getAvailableCores: " + Arrays.toString(returnVal));
return returnVal;
}
/**
* Gets the list of cores that are currently installed as Dynamic Feature Modules.
*
* @return the list of installed cores
*/
public String[] getInstalledCores() {
SplitInstallManager manager = SplitInstallManagerFactory.create(this);
String[] modules = manager.getInstalledModules().toArray(new String[0]);
List<String> cores = new ArrayList<>();
SharedPreferences prefs = UserPreferences.getPreferences(this);
for(int i = 0; i < modules.length; i++) {
String coreName = unsanitizeCoreName(modules[i]);
if(!prefs.getBoolean("core_deleted_" + coreName, false)) {
cores.add(coreName);
}
}
String[] returnVal = cores.toArray(new String[0]);
Log.i("RetroActivity", "getInstalledCores: " + Arrays.toString(returnVal));
return returnVal;
}
/**
* Asks the system to download a core.
*
* @param coreName Name of the core to install
*/
public void downloadCore(final String coreName) {
Log.i("RetroActivity", "downloadCore: " + coreName);
SharedPreferences prefs = UserPreferences.getPreferences(this);
prefs.edit().remove("core_deleted_" + coreName).apply();
SplitInstallManager manager = SplitInstallManagerFactory.create(this);
SplitInstallRequest request = SplitInstallRequest.newBuilder()
.addModule(sanitizeCoreName(coreName))
.build();
manager.startInstall(request)
.addOnSuccessListener(new OnSuccessListener<Integer>() {
@Override
public void onSuccess(Integer result) {
coreInstallInitiated(coreName, true);
}
})
.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
coreInstallInitiated(coreName, false);
}
});
}
/**
* Asks the system to delete a core.
*
* Note that the actual module deletion will not happen immediately (the OS will delete
* it whenever it feels like it), but the symlink will still be immediately removed.
*
* @param coreName Name of the core to delete
*/
public void deleteCore(String coreName) {
Log.i("RetroActivity", "deleteCore: " + coreName);
String newFilename = getCorePath() + coreName + "_libretro_android.so";
new File(newFilename).delete();
SharedPreferences prefs = UserPreferences.getPreferences(this);
prefs.edit().putBoolean("core_deleted_" + coreName, true).apply();
SplitInstallManager manager = SplitInstallManagerFactory.create(this);
manager.deferredUninstall(Collections.singletonList(sanitizeCoreName(coreName)));
}
/////////////// JNI methods ///////////////
/**
* Called when a core install is initiated.
*
* @param coreName Name of the core that the install is initiated for.
* @param successful true if success, false if failure
*/
private native void coreInstallInitiated(String coreName, boolean successful);
/**
* Called when the status of a core install has changed.
*
* @param coreNames Names of all cores that are currently being downloaded.
* @param status One of INSTALL_STATUS_DOWNLOADING, INSTALL_STATUS_INSTALLING,
* INSTALL_STATUS_INSTALLED, or INSTALL_STATUS_FAILED
* @param bytesDownloaded Number of bytes downloaded.
* @param totalBytesToDownload Total number of bytes to download.
*/
private native void coreInstallStatusChanged(String[] coreNames, int status, long bytesDownloaded, long totalBytesToDownload);
/////////////// Private methods ///////////////
/**
* Sanitizes a core name so that it can be used when dealing with
* Dynamic Feature Modules. Needed because Gradle modules cannot use
* dashes, but we have at least one core name ("mesen-s") that uses them.
*
* @param coreName Name of the core to sanitize.
* @return The sanitized core name.
*/
private String sanitizeCoreName(String coreName) {
return coreName.replace('-', '_');
}
/**
* Unsanitizes a core name from its module name.
*
* @param coreName Name of the core to unsanitize.
* @return The unsanitized core name.
*/
private String unsanitizeCoreName(String coreName) {
if(coreName.equals("mesen_s")) {
return "mesen-s";
}
return coreName;
}
/**
* Gets the path to the RetroArch cores directory.
*
* @return The path to the RetroArch cores directory
*/
private String getCorePath() {
return getApplicationInfo().dataDir + "/cores/";
}
/**
* Triggers a symlink update in the known places that Dynamic Feature Modules
* are installed to.
*/
private void updateSymlinks() {
traverseFilesystem(getFilesDir());
traverseFilesystem(new File(getApplicationInfo().nativeLibraryDir));
}
/**
* Traverse the filesystem, looking for native libraries.
* Symlinks any libraries it finds to the main RetroArch "cores" folder,
* updating any existing symlinks with the correct path to the native libraries.
*
* This is necessary because Dynamic Feature Modules are first downloaded
* and installed to a temporary location on disk, before being moved
* to a more permanent location by the system at a later point.
*
* This could probably be done in native code instead, if that's preferred.
*
* @param file The parent directory of the tree to traverse.
* @param cores List of cores to update.
* @param filenames List of filenames to update.
*/
private void traverseFilesystem(File file) {
File[] list = file.listFiles();
if(list == null) return;
// Check each file in a directory to see if it's a native library.
for(int i = 0; i < list.length; i++) {
File child = list[i];
String name = child.getName();
if(name.startsWith("lib") && name.endsWith(".so") && !name.contains("retroarch-activity")) {
// Found a native library!
String core = name.subSequence(3, name.length() - 3).toString();
String filename = child.getAbsolutePath();
SharedPreferences prefs = UserPreferences.getPreferences(this);
if(!prefs.getBoolean("core_deleted_" + core, false)) {
// Generate the destination filename and delete any existing symlinks / cores
String newFilename = getCorePath() + core + "_libretro_android.so";
new File(newFilename).delete();
try {
// On Android 5.0+, use the official API for creating a symlink.
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Os.symlink(filename, newFilename);
} else {
// On older versions, resort to using reflection instead.
Class<?> clazz = Class.forName("libcore.io.Libcore");
Field field = clazz.getDeclaredField("os");
field.setAccessible(true);
Object os = field.get(null);
Method method = os.getClass().getMethod("symlink", String.class, String.class);
method.invoke(os, filename, newFilename);
}
} catch (Exception e) {
// Symlink failed to be created. Should never happen.
}
}
} else if(file.isDirectory()) {
// Found another directory, so traverse it
traverseFilesystem(child);
}
}
}
}

View File

@ -2,4 +2,10 @@
.externalNativeBuild
build
phoenix.iml
output.json
keystore.properties
modules/
settings.gradle
dynamic_features.gradle
res/values/core_names.xml
res/values/module_names_*.xml

View File

@ -16,12 +16,14 @@
<uses-permission android:name="android.permission.VIBRATE" />
<application
android:name="com.google.android.play.core.splitcompat.SplitCompatApplication"
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:hasCode="true"
android:isGame="true"
android:banner="@drawable/banner"
android:extractNativeLibs="true"
tools:ignore="UnusedAttribute">
<activity android:name="com.retroarch.browser.mainmenu.MainMenuActivity" android:exported="true" android:launchMode="singleInstance">
<intent-filter>

View File

@ -37,12 +37,14 @@ android {
productFlavors {
normal {
resValue "string", "app_name", "RetroArch"
buildConfigField "boolean", "PLAY_STORE_BUILD", "false"
dimension "variant"
}
aarch64 {
applicationIdSuffix '.aarch64'
resValue "string", "app_name", "RetroArch (AArch64)"
buildConfigField "boolean", "PLAY_STORE_BUILD", "false"
dimension "variant"
ndk {
@ -52,12 +54,29 @@ android {
ra32 {
applicationIdSuffix '.ra32'
resValue "string", "app_name", "RetroArch (32-bit)"
buildConfigField "boolean", "PLAY_STORE_BUILD", "false"
dimension "variant"
ndk {
abiFilters 'armeabi-v7a', 'x86'
}
}
playStoreNormal {
resValue "string", "app_name", "RetroArch"
buildConfigField "boolean", "PLAY_STORE_BUILD", "true"
dimension "variant"
}
playStoreAarch64 {
applicationIdSuffix '.aarch64'
resValue "string", "app_name", "RetroArch (AArch64)"
buildConfigField "boolean", "PLAY_STORE_BUILD", "true"
dimension "variant"
ndk {
abiFilters 'arm64-v8a', 'x86_64'
}
}
}
sourceSets {
@ -67,10 +86,10 @@ android {
java.srcDirs = ['src', '../phoenix-common/src']
jniLibs.srcDir '../phoenix-common/libs'
jni.srcDirs = []
res.srcDirs = ['../phoenix-common/res']
res.srcDirs = ['res', '../phoenix-common/res']
}
aarch64 {
res.srcDirs = ['res64']
res.srcDirs = ['res', 'res64']
}
}
@ -104,3 +123,12 @@ android {
}
}
}
dependencies {
implementation 'com.google.android.play:core:1.8.0'
}
def dynamicFeatures = file("dynamic_features.gradle")
if(dynamicFeatures.exists()) {
apply from: "dynamic_features.gradle"
}

View File

@ -0,0 +1,100 @@
#!/bin/bash
# This script generates Gradle modules for each Android core,
# so that they can be served by Google Play as Dynamic Feature Modules.
# Run "./init_modules.sh" to generate modules, or "./init_modules.sh clean" to remove them
# These paths assume that this script is running inside libretro-super,
# and that the compiled Android cores are available while this script is run
RECIPES_PATH="../../../../recipes/android"
INFO_PATH="../../../../dist/info"
CORES_PATH="../../../../dist/android"
# Get the list of Android cores to generate modules for
CORES_LIST=$(cat module_list.txt)
# The below command would generate a module for every single Android core,
# but Dynamic Feature Modules enforces a 50-module limit
#CORES_LIST=$(find $RECIPES_PATH -type f ! -name '*.*' -exec cat {} + | awk '{ split($1, test, " "); print test[1] }' | grep "\S")
# Delete any leftover files from previous script runs
rm -rf modules
rm -f res/values/core_names.xml
rm -f res/values/module_names_*.xml
rm -f dynamic_features.gradle
rm -f settings.gradle
if [[ $1 = clean ]] ; then
exit 1
fi
# Make directory for modules to be stored in
mkdir -p modules
mkdir -p res/values
# Begin generating files with necessary metadata
# for compiling Dynamic Feature Modules
echo "<resources>" >> res/values/core_names.xml
echo "android {" >> dynamic_features.gradle
echo "dynamicFeatures = [" >> dynamic_features.gradle
for arch in armeabi-v7a arm64-v8a x86 x86_64
do
SANITIZED_ARCH_NAME=$(echo $arch | sed "s/-/_/g")
echo "<resources>" >> res/values/module_names_$arch.xml
echo "<string-array name=\"module_names_$SANITIZED_ARCH_NAME\">" >> res/values/module_names_$arch.xml
done
# Time to generate a module for each core!
while IFS= read -r core; do
SANITIZED_CORE_NAME=$(echo $core | sed "s/-/_/g")
DISPLAY_NAME=$(cat $INFO_PATH/${core}_libretro.info | grep "display_name" | cut -d'"' -f 2)
echo "Generating module for $core..."
# Make a copy of the template
cp -r module_template modules/$SANITIZED_CORE_NAME
# Write the name of the core into AndroidManifest.xml
if [[ "$OSTYPE" == "darwin"* ]]
then
sed -i '' "s/%CORE_NAME%/$SANITIZED_CORE_NAME/g" modules/$SANITIZED_CORE_NAME/AndroidManifest.xml
else
sed -i "s/%CORE_NAME%/$SANITIZED_CORE_NAME/g" modules/$SANITIZED_CORE_NAME/AndroidManifest.xml
fi
# Create a libs directory for each architecture,
# and copy the libretro core into each directory
for arch in armeabi-v7a arm64-v8a x86 x86_64
do
mkdir -p modules/$SANITIZED_CORE_NAME/libs/$arch
if [[ -e $CORES_PATH/$arch/${core}_libretro_android.so ]]
then
ln -s ../../../../$CORES_PATH/$arch/${core}_libretro_android.so modules/$SANITIZED_CORE_NAME/libs/$arch/lib$core.so
else
touch modules/$SANITIZED_CORE_NAME/libs/$arch/lib$core.so
fi
if [[ -s "modules/$SANITIZED_CORE_NAME/libs/$arch/lib$core.so" ]]
then
echo "<item>$core</item>" >> res/values/module_names_$arch.xml
fi
done
# Write metadata about the module into the corresponding files
echo "<string name=\"core_name_$SANITIZED_CORE_NAME\">$DISPLAY_NAME</string>" >> res/values/core_names.xml
echo "':modules:$SANITIZED_CORE_NAME'," >> dynamic_features.gradle
echo "include ':modules:$SANITIZED_CORE_NAME'" >> settings.gradle
done <<< "$CORES_LIST"
# Finish generating the metadata files
echo "</resources>" >> res/values/core_names.xml
echo "]" >> dynamic_features.gradle
echo "}" >> dynamic_features.gradle
for arch in armeabi-v7a arm64-v8a x86 x86_64
do
echo "</string-array>" >> res/values/module_names_$arch.xml
echo "</resources>" >> res/values/module_names_$arch.xml
done

View File

@ -0,0 +1,5 @@
genesis_plus_gx
mesen-s
dolphin
mupen64plus_next_gles3
flycast

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:dist="http://schemas.android.com/apk/distribution"
package="com.retroarch.core_modules.%CORE_NAME%">
<dist:module dist:title="@string/core_name_%CORE_NAME%">
<dist:delivery>
<dist:on-demand />
</dist:delivery>
<dist:fusing dist:include="true" />
</dist:module>
<application
android:hasCode="false"
android:extractNativeLibs="true" />
</manifest>

View File

@ -0,0 +1,49 @@
apply plugin: 'com.android.dynamic-feature'
android {
compileSdkVersion 28
defaultConfig {
minSdkVersion 16
targetSdkVersion 28
}
flavorDimensions "variant"
productFlavors {
normal {
dimension "variant"
}
aarch64 {
dimension "variant"
ndk {
abiFilters 'arm64-v8a', 'x86_64'
}
}
ra32 {
dimension "variant"
ndk {
abiFilters 'armeabi-v7a', 'x86'
}
}
playStoreNormal {
dimension "variant"
}
playStoreAarch64 {
dimension "variant"
ndk {
abiFilters 'arm64-v8a', 'x86_64'
}
}
}
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
jniLibs.srcDirs = ['libs']
}
}
}
dependencies {
implementation rootProject
}