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>
</GradleProjectSettings>
</option>
<option name="offlineMode" value="true" />
</component>
</project>

View File

@ -3,23 +3,5 @@
"assetPath":"mods/virtual-keyboard.zip",
"Name": "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/",
"assetPath": "assemblies/*.dll",
"assetPath": "assemblies/",
"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_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.widget.ImageView;
import androidx.annotation.RequiresApi;
import androidx.documentfile.provider.DocumentFile;
import com.afollestad.materialdialogs.MaterialDialog;
import com.fasterxml.jackson.core.type.TypeReference;
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.ZipUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.filefilter.WildcardFileFilter;
import org.zeroturnaround.zip.ZipUtil;
@ -48,6 +52,7 @@ import java.io.FileFilter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.Channels;
import java.util.ArrayList;
import java.util.Collections;
@ -58,9 +63,6 @@ import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
import androidx.annotation.RequiresApi;
import androidx.documentfile.provider.DocumentFile;
import pxb.android.axml.AxmlReader;
import pxb.android.axml.AxmlVisitor;
import pxb.android.axml.AxmlWriter;
@ -215,7 +217,7 @@ public class CommonLogic {
* @param versionCode 版本号
* @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);
filterManifest(apkFilesManifests, packageName, versionCode);
List<ManifestEntry> manifestEntries = null;
@ -270,15 +272,49 @@ public class CommonLogic {
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;
}
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) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
File pathFrom = new File(FileUtils.getStadewValleyBasePath(), "Android/obb/" + Constants.ORIGIN_PACKAGE_NAME_GOOGLE);
File pathTo = new File(FileUtils.getStadewValleyBasePath(), "StardewValley");
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;
}
if (!pathTo.exists()) {
@ -314,7 +350,7 @@ public class CommonLogic {
} catch (IOException ignore) {
}
} 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) {
Uri targetDirUri = pathToUri("Android/data/" + Constants.ORIGIN_PACKAGE_NAME_GOOGLE);
if(checkPathPermission(context, targetDirUri)) {
public static boolean checkDataRootPermission(Activity context) {
File pathFrom = new File(FileUtils.getStadewValleyBasePath(), "Android/data/" + Constants.TARGET_PACKAGE_NAME + "/files/");
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;
}
ActivityResultHandler.registerListener(ActivityResultHandler.REQUEST_CODE_DATA_FILES_ACCESS_PERMISSION, (resultCode, data) -> {
@ -504,31 +556,31 @@ public class CommonLogic {
if (uri == null) {
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);
} else {
callback.accept(false);
}
}
public static Uri pathToUri(String path) {
return Uri.parse("content://com.android.externalstorage.documents/tree/primary%3A" + path.replace("/", "%3A"));
public static Uri pathToTreeUri(String path) {
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) {
if(DocumentFile.fromTreeUri(context, targetDirUri).canWrite()) {
if (DocumentFile.fromTreeUri(context, targetDirUri).canWrite()) {
return true;
}
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) {
Uri targetDirUri = pathToUri("Android/obb");
if(checkPathPermission(context, targetDirUri)) {
Uri targetDirUri = pathToTreeUri("Android/obb");
if (checkPathPermission(context, targetDirUri)) {
return true;
}
ActivityResultHandler.registerListener(ActivityResultHandler.REQUEST_CODE_OBB_FILES_ACCESS_PERMISSION, (resultCode, data) -> {

View File

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