Fix for Android SAF framework

This commit is contained in:
zhiyang7 2023-04-11 15:50:16 +08:00
parent c7fcaa26f3
commit 6b101f394d
11 changed files with 89 additions and 64 deletions

View File

@ -16,6 +16,5 @@
</option> </option>
</GradleProjectSettings> </GradleProjectSettings>
</option> </option>
<option name="offlineMode" value="true" />
</component> </component>
</project> </project>

View File

@ -3,23 +3,5 @@
"assetPath":"mods/virtual-keyboard.zip", "assetPath":"mods/virtual-keyboard.zip",
"Name": "VirtualKeyboard", "Name": "VirtualKeyboard",
"UniqueID": "VirtualKeyboard" "UniqueID": "VirtualKeyboard"
},
{
"assetPath":"mods/custom-localization.zip",
"Name": "CustomLocalization",
"UniqueID": "ZaneYork.CustomLocalization",
"CleanInstall": true,
"Version": "1.1.0",
"OriginUniqueId": ["SMAPI.CustomLocalization"]
},
{
"assetPath":"mods/console-commands.zip",
"Name": "Console Commands",
"UniqueID": "SMAPI.ConsoleCommands"
},
{
"assetPath":"mods/save-backup.zip",
"Name": "SaveBackup",
"UniqueID": "SMAPI.SaveBackup"
} }
] ]

View File

@ -11,7 +11,7 @@
}, },
{ {
"targetPath": "smapi-internal/", "targetPath": "smapi-internal/",
"assetPath": "assemblies/*.dll", "assetPath": "assemblies/",
"origin": 1 "origin": 1
} }
] ]

View File

@ -26,6 +26,8 @@ public class Constants {
* 安装包目标包名 * 安装包目标包名
*/ */
public static final String TARGET_PACKAGE_NAME = "com.zane.stardewvalley"; public static final String TARGET_PACKAGE_NAME = "com.zane.stardewvalley";
public static final String TARGET_DATA_FILE_URI = "Android/data/" + Constants.TARGET_PACKAGE_NAME;
/** /**
* 安装包目标包名 * 安装包目标包名
*/ */

View File

@ -20,6 +20,9 @@ import android.view.animation.Animation;
import android.view.animation.AnimationUtils; import android.view.animation.AnimationUtils;
import android.widget.ImageView; import android.widget.ImageView;
import androidx.annotation.RequiresApi;
import androidx.documentfile.provider.DocumentFile;
import com.afollestad.materialdialogs.MaterialDialog; import com.afollestad.materialdialogs.MaterialDialog;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
@ -39,6 +42,7 @@ import com.zane.smapiinstaller.utils.FileUtils;
import com.zane.smapiinstaller.utils.StringUtils; import com.zane.smapiinstaller.utils.StringUtils;
import com.zane.smapiinstaller.utils.ZipUtils; import com.zane.smapiinstaller.utils.ZipUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.filefilter.WildcardFileFilter; import org.apache.commons.io.filefilter.WildcardFileFilter;
import org.zeroturnaround.zip.ZipUtil; import org.zeroturnaround.zip.ZipUtil;
@ -48,6 +52,7 @@ import java.io.FileFilter;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.Channels; import java.nio.channels.Channels;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -58,9 +63,6 @@ import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Stream; import java.util.stream.Stream;
import androidx.annotation.RequiresApi;
import androidx.documentfile.provider.DocumentFile;
import pxb.android.axml.AxmlReader; import pxb.android.axml.AxmlReader;
import pxb.android.axml.AxmlVisitor; import pxb.android.axml.AxmlVisitor;
import pxb.android.axml.AxmlWriter; import pxb.android.axml.AxmlWriter;
@ -215,7 +217,7 @@ public class CommonLogic {
* @param versionCode 版本号 * @param versionCode 版本号
* @return 操作是否成功 * @return 操作是否成功
*/ */
public static boolean unpackSmapiFiles(Context context, String apkPath, boolean checkMode, String packageName, long versionCode) { public static boolean unpackSmapiFiles(Activity context, String apkPath, boolean checkMode, String packageName, long versionCode) {
List<ApkFilesManifest> apkFilesManifests = CommonLogic.findAllApkFileManifest(context); List<ApkFilesManifest> apkFilesManifests = CommonLogic.findAllApkFileManifest(context);
filterManifest(apkFilesManifests, packageName, versionCode); filterManifest(apkFilesManifests, packageName, versionCode);
List<ManifestEntry> manifestEntries = null; List<ManifestEntry> manifestEntries = null;
@ -270,15 +272,49 @@ public class CommonLogic {
break; break;
} }
} }
if (CommonLogic.checkDataRootPermission(context)) {
Uri targetDirUri = pathToTreeUri(Constants.TARGET_DATA_FILE_URI);
DocumentFile documentFile = DocumentFile.fromTreeUri(context, targetDirUri);
for (DocumentFile file : documentFile.listFiles()) {
if (file.getName().equals("files")) {
copyDocument(context, new File(basePath, "smapi-internal"), file);
copyDocument(context, new File(basePath, "Mods"), file);
}
}
}
return true; return true;
} }
private static void copyDocument(Activity context, File src, DocumentFile dest) {
if (src.isDirectory()) {
DocumentFile documentFile = dest.findFile(src.getName());
if (documentFile == null) {
documentFile = dest.createDirectory(src.getName());
}
for (File file : src.listFiles()) {
copyDocument(context, file, documentFile);
}
} else {
DocumentFile documentFile = dest.findFile(src.getName());
if (documentFile == null) {
documentFile = dest.createFile("application/x-binary", src.getName());
}
if(documentFile.length() != src.length()) {
try (OutputStream outputStream = context.getContentResolver().openOutputStream(documentFile.getUri())) {
FileUtils.copy(src, outputStream);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
private static boolean checkMusic(Context context) { private static boolean checkMusic(Context context) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
File pathFrom = new File(FileUtils.getStadewValleyBasePath(), "Android/obb/" + Constants.ORIGIN_PACKAGE_NAME_GOOGLE); File pathFrom = new File(FileUtils.getStadewValleyBasePath(), "Android/obb/" + Constants.ORIGIN_PACKAGE_NAME_GOOGLE);
File pathTo = new File(FileUtils.getStadewValleyBasePath(), "StardewValley"); File pathTo = new File(FileUtils.getStadewValleyBasePath(), "StardewValley");
if (pathFrom.exists() && pathFrom.isDirectory()) { if (pathFrom.exists() && pathFrom.isDirectory()) {
if(!checkObbRootPermission((Activity) context, ActivityResultHandler.REQUEST_CODE_OBB_FILES_ACCESS_PERMISSION, (success) -> checkMusic(context))) { if (!checkObbRootPermission((Activity) context, ActivityResultHandler.REQUEST_CODE_OBB_FILES_ACCESS_PERMISSION, (success) -> checkMusic(context))) {
return false; return false;
} }
if (!pathTo.exists()) { if (!pathTo.exists()) {
@ -314,7 +350,7 @@ public class CommonLogic {
} catch (IOException ignore) { } catch (IOException ignore) {
} }
} else { } else {
ZipUtil.unpackEntry(new File(apkPath), entry.getAssetPath(), targetFile); ZipUtil.unpack(new File(apkPath), targetFile, name -> name.startsWith(entry.getAssetPath()) ? FilenameUtils.getName(name) : null);
} }
} }
} }
@ -476,9 +512,25 @@ public class CommonLogic {
} }
} }
public static boolean checkDataRootPermission(Activity context, int REQUEST_CODE_FOR_DIR, Consumer<Boolean> callback) { public static boolean checkDataRootPermission(Activity context) {
Uri targetDirUri = pathToUri("Android/data/" + Constants.ORIGIN_PACKAGE_NAME_GOOGLE); File pathFrom = new File(FileUtils.getStadewValleyBasePath(), "Android/data/" + Constants.TARGET_PACKAGE_NAME + "/files/");
if(checkPathPermission(context, targetDirUri)) { if (!pathFrom.exists()) {
return false;
}
Uri targetDirUri = pathToTreeUri(Constants.TARGET_DATA_FILE_URI);
if (checkPathPermission(context, targetDirUri)) {
return true;
}
return false;
}
public static boolean requestDataRootPermission(Activity context, int REQUEST_CODE_FOR_DIR, Consumer<Boolean> callback) {
File pathFrom = new File(FileUtils.getStadewValleyBasePath(), "Android/data/" + Constants.TARGET_PACKAGE_NAME + "/files");
if (!pathFrom.exists()) {
return true;
}
Uri targetDirUri = pathToTreeUri(Constants.TARGET_DATA_FILE_URI);
if (checkPathPermission(context, targetDirUri)) {
return true; return true;
} }
ActivityResultHandler.registerListener(ActivityResultHandler.REQUEST_CODE_DATA_FILES_ACCESS_PERMISSION, (resultCode, data) -> { ActivityResultHandler.registerListener(ActivityResultHandler.REQUEST_CODE_DATA_FILES_ACCESS_PERMISSION, (resultCode, data) -> {
@ -504,32 +556,32 @@ public class CommonLogic {
if (uri == null) { if (uri == null) {
return; return;
} }
context.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); context.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
callback.accept(true); callback.accept(true);
} else { } else {
callback.accept(false); callback.accept(false);
} }
} }
public static Uri pathToUri(String path) { public static Uri pathToTreeUri(String path) {
return Uri.parse("content://com.android.externalstorage.documents/tree/primary%3A" + path.replace("/", "%3A")); return Uri.parse("content://com.android.externalstorage.documents/tree/primary%3A" + path.replace("/", "%2F"));
}
public static Uri pathToSingleUri(String path) {
return Uri.parse("content://com.android.externalstorage.documents/document/primary%3A" + path.replace("/", "%2F"));
} }
public static boolean checkPathPermission(Context context, Uri targetDirUri) { public static boolean checkPathPermission(Context context, Uri targetDirUri) {
if(DocumentFile.fromTreeUri(context, targetDirUri).canWrite()) { if (DocumentFile.fromTreeUri(context, targetDirUri).canWrite()) {
return true; return true;
} }
return false; return false;
} }
public static boolean checkPathPermission(Context context, String path) {
return checkPathPermission(context, path);
}
public static boolean checkObbRootPermission(Activity context, int REQUEST_CODE_FOR_DIR, Consumer<Boolean> callback) { public static boolean checkObbRootPermission(Activity context, int REQUEST_CODE_FOR_DIR, Consumer<Boolean> callback) {
Uri targetDirUri = pathToUri("Android/obb"); Uri targetDirUri = pathToTreeUri("Android/obb");
if(checkPathPermission(context, targetDirUri)) { if (checkPathPermission(context, targetDirUri)) {
return true; return true;
} }
ActivityResultHandler.registerListener(ActivityResultHandler.REQUEST_CODE_OBB_FILES_ACCESS_PERMISSION, (resultCode, data) -> { ActivityResultHandler.registerListener(ActivityResultHandler.REQUEST_CODE_OBB_FILES_ACCESS_PERMISSION, (resultCode, data) -> {
takePermission(resultCode, data, context.getContentResolver(), callback); takePermission(resultCode, data, context.getContentResolver(), callback);

View File

@ -15,23 +15,21 @@ import com.zane.smapiinstaller.constant.Constants;
import com.zane.smapiinstaller.constant.DialogAction; import com.zane.smapiinstaller.constant.DialogAction;
import com.zane.smapiinstaller.databinding.FragmentInstallBinding; import com.zane.smapiinstaller.databinding.FragmentInstallBinding;
import com.zane.smapiinstaller.dto.Tuple2; import com.zane.smapiinstaller.dto.Tuple2;
import com.zane.smapiinstaller.logic.ActivityResultHandler;
import com.zane.smapiinstaller.logic.ApkPatcher; import com.zane.smapiinstaller.logic.ApkPatcher;
import com.zane.smapiinstaller.logic.CommonLogic; import com.zane.smapiinstaller.logic.CommonLogic;
import com.zane.smapiinstaller.logic.ModAssetsManager;
import com.zane.smapiinstaller.ui.main.MainTabsFragmentDirections; import com.zane.smapiinstaller.ui.main.MainTabsFragmentDirections;
import com.zane.smapiinstaller.utils.ConfigUtils; import com.zane.smapiinstaller.utils.ConfigUtils;
import com.zane.smapiinstaller.utils.DialogUtils; import com.zane.smapiinstaller.utils.DialogUtils;
import com.zane.smapiinstaller.utils.FileUtils; import com.zane.smapiinstaller.utils.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.RegExUtils; import org.apache.commons.lang3.RegExUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
@ -115,9 +113,9 @@ public class InstallFragment extends Fragment {
* 安装逻辑 * 安装逻辑
*/ */
private void installLogic(boolean isAdv) { private void installLogic(boolean isAdv) {
// if (!CommonLogic.checkDataRootPermission(context, ActivityResultHandler.REQUEST_CODE_DATA_FILES_ACCESS_PERMISSION, (success) -> installLogic(isAdv))) { if (!CommonLogic.requestDataRootPermission(context, ActivityResultHandler.REQUEST_CODE_DATA_FILES_ACCESS_PERMISSION, (success) -> installLogic(isAdv))) {
// return; return;
// } }
if (task != null) { if (task != null) {
task.interrupt(); task.interrupt();
} }
@ -138,14 +136,6 @@ public class InstallFragment extends Fragment {
DialogUtils.showAlertDialog(binding.getRoot(), R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.error_game_not_found))); DialogUtils.showAlertDialog(binding.getRoot(), R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.error_game_not_found)));
return; return;
} }
DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.unpacking_smapi_files, null);
// if (!CommonLogic.unpackSmapiFiles(context, apkPath, false, patcher.getGamePackageName(), patcher.getGameVersionCode())) {
// DialogUtils.showAlertDialog(binding.getRoot(), R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.failed_to_unpack_smapi_files)));
// return;
// }
// ModAssetsManager modAssetsManager = new ModAssetsManager(binding.getRoot());
// DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.unpacking_smapi_files, 6);
// modAssetsManager.installDefaultMods();
DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.patching_package, 8); DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.patching_package, 8);
File targetApk = new File(dest, "base.apk"); File targetApk = new File(dest, "base.apk");
if (!patcher.patch(paths.getFirst(), paths.getSecond(), targetApk, isAdv, false)) { if (!patcher.patch(paths.getFirst(), paths.getSecond(), targetApk, isAdv, false)) {
@ -162,21 +152,21 @@ public class InstallFragment extends Fragment {
} }
return; return;
} }
// List<String> resourcePacks = new ArrayList<>(); DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.unpacking_smapi_files, null);
// if (paths.getSecond() != null) { if (!CommonLogic.unpackSmapiFiles(context, targetApk.getAbsolutePath(), false, patcher.getGamePackageName(), patcher.getGameVersionCode())) {
// for (String resourcePack : paths.getSecond()) { DialogUtils.showAlertDialog(binding.getRoot(), R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.failed_to_unpack_smapi_files)));
// File targetResourcePack = new File(dest, FilenameUtils.getName(resourcePack)); return;
// patcher.patch(resourcePack, paths.getSecond(), targetResourcePack, false, true); }
// resourcePacks.add(targetResourcePack.getAbsolutePath()); ModAssetsManager modAssetsManager = new ModAssetsManager(binding.getRoot());
// } DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.unpacking_smapi_files, 6);
// } modAssetsManager.installDefaultMods();
DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.signing_package, null); DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.signing_package, null);
String signPath = patcher.sign(targetApk.getAbsolutePath()); String signPath = patcher.sign(targetApk.getAbsolutePath());
if (signPath == null) { if (signPath == null) {
DialogUtils.showAlertDialog(binding.getRoot(), R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.failed_to_sign_game))); DialogUtils.showAlertDialog(binding.getRoot(), R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.failed_to_sign_game)));
return; return;
} }
// List<String> signedResourcePacks = resourcePacks.stream().map(patcher::sign).collect(Collectors.toList());
DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.installing_package, null); DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.installing_package, null);
patcher.install(signPath); patcher.install(signPath);
})); }));