diff --git a/android/app/build.gradle b/android/app/build.gradle index 2f881f4..36f4606 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -147,17 +147,26 @@ android { } } } + packagingOptions { + //当项目中出现多个同名的so文件会编译异常,过滤多余的so文件 + pickFirst '**/libfbjni.so' + pickFirst '**/libc++_shared.so' + pickFirst '**/libjsc.so' + } } dependencies { - implementation project(':@react-native-community_async-storage') - implementation project(':react-native-music-control') - implementation project(':react-native-vector-icons') - implementation project(':react-native-gesture-handler') - implementation project(':react-native-video') + implementation project(path: ':@react-native-community_toolbar-android') + implementation project(path: ':@react-native-community_async-storage') + implementation project(path: ':react-native-music-control') + implementation project(path: ':react-native-vector-icons') + implementation project(path: ':react-native-gesture-handler') + implementation project(path: ':react-native-screens') + implementation project(path: ':react-native-video') implementation fileTree(dir: "libs", include: ["*.jar"]) - implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}" + implementation "androidx.activity:activity:1.3.0" implementation "com.facebook.react:react-native:+" // From node_modules + implementation "org.webkit:android-jsc:+" } // Run this once to be able to run the application with BUCK diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 262c066..6aec55c 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,31 +1,35 @@ + package="com.listen1.app"> - - - - - - - - + android:requestLegacyExternalStorage="true" + android:roundIcon="@mipmap/ic_launcher_round" + android:theme="@style/AppTheme" + android:usesCleartextTraffic="true"> + + + + + + + + + diff --git a/android/app/src/main/java/com/listen1/app/MainApplication.java b/android/app/src/main/java/com/listen1/app/MainApplication.java index 2a4c5e4..a8ae68b 100644 --- a/android/app/src/main/java/com/listen1/app/MainApplication.java +++ b/android/app/src/main/java/com/listen1/app/MainApplication.java @@ -29,12 +29,13 @@ public boolean getUseDeveloperSupport() { @Override protected List getPackages() { return Arrays.asList( - new MainReactPackage(), - new AsyncStoragePackage(), - new MusicControl(), - new VectorIconsPackage(), - new RNGestureHandlerPackage(), - new ReactVideoPackage() + new MainReactPackage(), + new MainPackage(), + new AsyncStoragePackage(), + new MusicControl(), + new VectorIconsPackage(), + new RNGestureHandlerPackage(), + new ReactVideoPackage() ); } diff --git a/android/app/src/main/java/com/listen1/app/MainPackage.java b/android/app/src/main/java/com/listen1/app/MainPackage.java new file mode 100644 index 0000000..ac77479 --- /dev/null +++ b/android/app/src/main/java/com/listen1/app/MainPackage.java @@ -0,0 +1,35 @@ +package com.listen1.app; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; +import com.listen1.app.utils.FileConfig; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.annotation.Nonnull; + +/** + * @Description: java类作用描述 + * @Author: Void + * @CreateDate: 2023/1/11 15:31 + * @UpdateDate: 2023/1/11 15:31 + */ +public class MainPackage implements ReactPackage { + @Nonnull + @Override + public List createNativeModules(@Nonnull ReactApplicationContext context) { + List modules = new ArrayList<>(); + modules.add(new FileConfig(context)); + return modules; + } + + @Nonnull + @Override + public List createViewManagers(@Nonnull ReactApplicationContext reactApplicationContext) { + return Collections.emptyList(); + } +} diff --git a/android/app/src/main/java/com/listen1/app/OpenFileActivity.java b/android/app/src/main/java/com/listen1/app/OpenFileActivity.java new file mode 100644 index 0000000..5540d9b --- /dev/null +++ b/android/app/src/main/java/com/listen1/app/OpenFileActivity.java @@ -0,0 +1,148 @@ +package com.listen1.app; + +import android.Manifest; +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.os.ParcelFileDescriptor; +import android.util.Log; +import androidx.annotation.Nullable; +import com.listen1.app.utils.FileConfig; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; + +public class OpenFileActivity extends PermissionActivity { + private final String[] readStoragePermissions = new String[]{ + Manifest.permission.READ_EXTERNAL_STORAGE + }; + private int openFileRequestCode = -1; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + openFileRequestCode = getIntent().getIntExtra("OPEN_FILE_REQUEST_CODE", -1); + checkPermission(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode != 1001) return; + if (data == null) { + backResult("", false); + return; + } + byte[] bytes = new byte[50]; + StringBuilder builder = new StringBuilder(); + Uri uri = data.getData(); + new Thread() { + @Override + public void run() { + super.run(); + FileInputStream fileInputStream = null; + ParcelFileDescriptor descriptor = null; + int endLength; + try { + descriptor = getContentResolver().openFileDescriptor(uri, "r"); + fileInputStream = new FileInputStream(descriptor.getFileDescriptor()); + while ((endLength = fileInputStream.read(bytes)) > 0) { + builder.append(new String(bytes, 0, endLength)); + } + } catch (IOException e) { + e.printStackTrace(); + backResult("", false); + } finally { + try { + if (descriptor != null) descriptor.close(); + if (fileInputStream != null) fileInputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + backResult(builder.toString(), false); + } + } + }.start(); + } + + @Override + protected String[] getPermissionArray() { + return readStoragePermissions; + } + + @Override + protected String getDialogHint() { + return "需要读写权限以继续流程"; + } + + /** + * 权限请求结果回调 + */ + @Override + protected void permissionResult(boolean state, @Nullable String msg) { + Log.d("PermissionActivity", "state:" + state); + if (!state) return; + if (openFileRequestCode == FileConfig.readFileCode) { + //打开文件管理器 + Intent mIntent = new Intent(Intent.ACTION_GET_CONTENT); + mIntent.addCategory(Intent.CATEGORY_OPENABLE); + mIntent.setType("application/json"); + startActivityForResult(mIntent, 1001); + } else if (openFileRequestCode == FileConfig.writeFileCode) { + writeContentToFile(); + } + } + + private void backResult(String content, boolean writeState) { + if (callback == null) return; + Bundle mBundle = new Bundle(); + if (openFileRequestCode == FileConfig.readFileCode) { + mBundle.putString("jsonContent", content); + } else if (openFileRequestCode == FileConfig.writeFileCode) { + mBundle.putBoolean("writeState", writeState); + } + callback.onResult(openFileRequestCode, mBundle); + callback = null; + finish(); + } + + private void writeContentToFile() { + File dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + boolean state; + if (!dir.exists()) dir.mkdirs(); + File exportJson = new File(dir, "listen1_backup.json"); + try { + if (exportJson.exists()) { + //如果文件存在,清空文件内容 + FileWriter fileWrite = new FileWriter(exportJson); + fileWrite.write(""); + fileWrite.flush(); + fileWrite.close(); + } else { + exportJson.createNewFile(); + } + PrintWriter writer = new PrintWriter(new FileOutputStream(exportJson)); + writer.print(FileConfig.content); + writer.close(); + state = true; + } catch (IOException e) { + e.printStackTrace(); + state = false; + } + backResult("", state); + } + + private static ResultCallback callback = null; + + public static void newInstance(Activity activity, int openFileRequestCode, @Nullable ResultCallback callback) { + OpenFileActivity.callback = callback; + Intent mIntent = new Intent(activity, OpenFileActivity.class); + mIntent.putExtra("OPEN_FILE_REQUEST_CODE", openFileRequestCode); + activity.startActivity(mIntent); + } +} diff --git a/android/app/src/main/java/com/listen1/app/PermissionActivity.java b/android/app/src/main/java/com/listen1/app/PermissionActivity.java new file mode 100644 index 0000000..8718a22 --- /dev/null +++ b/android/app/src/main/java/com/listen1/app/PermissionActivity.java @@ -0,0 +1,95 @@ +package com.listen1.app; + +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Bundle; +import android.provider.Settings; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +public abstract class PermissionActivity extends AppCompatActivity { + private String requestHint = "该操作需要权限才能继续"; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + String hint = getDialogHint(); + if (hint != null && !hint.isEmpty()) { + requestHint = hint; + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode != 1001) return; + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + permissionResult(true, null); + } + } + + protected abstract String[] getPermissionArray(); + + protected abstract String getDialogHint(); + + /** + * 返回权限处理结果 + * + * @param state true:权限获取成功 + * @param msg 额外的消息字符串 + */ + protected abstract void permissionResult(boolean state, @Nullable String msg); + + protected void checkPermission() { + if (getPermissionArray() == null || getPermissionArray().length == 0) { + throw new RuntimeException("权限不能为空!"); + } + for (String permission : getPermissionArray()) { + if (ContextCompat.checkSelfPermission(getApplicationContext(), permission) != PackageManager.PERMISSION_DENIED) + continue; + if (ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) { + //用户之前拒绝了权限,且设置了不再提示 + createDialog("已设置拒绝授予权限且不在显示,请前往设置手动设置权限", "确定", (dialog, which) -> { + //当用户还是想用APP,跳转到APP设置界面,由用户手动授予权限 + Intent mIntent = new Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", getPackageName(), null) + ); + startActivity(mIntent); + }); + permissionResult(false, null); + } else { + createDialog(requestHint, "设置权限", (dialog, which) -> ActivityCompat.requestPermissions(this, getPermissionArray(), 1001)); + } + return; + } + permissionResult(true, null); + } + + /** + * 创建请求权限弹窗,当用户允许时触发权限请求流程 + * + * @param msg 弹窗内容 + * @param positiveBtn acceptBtn + */ + private void createDialog(String msg, String positiveBtn, DialogInterface.OnClickListener listener) { + AlertDialog permissionDialog = new AlertDialog.Builder(this) + .setTitle("请求权限") + .setMessage(msg) + .setCancelable(false) + .setPositiveButton(positiveBtn, listener) + .setNegativeButton("取消", (dialog, which) -> finish()) + .create(); + if (permissionDialog.isShowing()) { + permissionDialog.dismiss(); + } + permissionDialog.show(); + } +} diff --git a/android/app/src/main/java/com/listen1/app/ResultCallback.java b/android/app/src/main/java/com/listen1/app/ResultCallback.java new file mode 100644 index 0000000..a03a639 --- /dev/null +++ b/android/app/src/main/java/com/listen1/app/ResultCallback.java @@ -0,0 +1,7 @@ +package com.listen1.app; + +import android.os.Bundle; + +public interface ResultCallback { + void onResult(int requestCode, Bundle bundle); +} diff --git a/android/app/src/main/java/com/listen1/app/utils/FileConfig.java b/android/app/src/main/java/com/listen1/app/utils/FileConfig.java new file mode 100644 index 0000000..deb03d9 --- /dev/null +++ b/android/app/src/main/java/com/listen1/app/utils/FileConfig.java @@ -0,0 +1,65 @@ +package com.listen1.app.utils; + +import android.os.Bundle; +import android.util.Log; + +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.listen1.app.OpenFileActivity; +import com.listen1.app.ResultCallback; + +import javax.annotation.Nonnull; +/** + * @Description: 歌曲配置的文件备份和恢复 + * @Author: Void + * @CreateDate: 2023/1/11 15:22 + * @UpdateDate: 2023/1/11 15:22 + */ +public class FileConfig extends ReactContextBaseJavaModule implements ResultCallback { + public static final int readFileCode = 101; + public static final int writeFileCode = 102; + public static String content; + private Callback resultCallback; + private Callback errorCallback; + + public FileConfig(@Nonnull ReactApplicationContext reactContext) { + super(reactContext); + } + + @Nonnull + @Override + public String getName() { + return "FileImportConfig"; + } + + @Override + public void onResult(int requestCode, Bundle bundle) { + if (requestCode == readFileCode) { + String content = bundle.getString("jsonContent"); + if (content == null || content.isEmpty()) { + errorCallback.invoke("read fail"); + } else { + Log.d(getName(), content); + resultCallback.invoke(content); + } + } else if (requestCode == writeFileCode) { + resultCallback.invoke(bundle.getBoolean("writeState")); + } + } + + @ReactMethod + public void readFile(Callback resultCallback, Callback errorCallback) { + this.resultCallback = resultCallback; + this.errorCallback = errorCallback; + OpenFileActivity.newInstance(getCurrentActivity(), readFileCode, this); + } + + @ReactMethod + public void writeFile(String content, Callback resultCallback) { + this.resultCallback = resultCallback; + FileConfig.content = content; + OpenFileActivity.newInstance(getCurrentActivity(), writeFileCode, this); + } +} diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index 319eb0c..158876d 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -5,4 +5,18 @@ + + diff --git a/android/build.gradle b/android/build.gradle index 3a1d305..3663e40 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,23 +2,30 @@ buildscript { ext { + //在导入的项目中,如:react-native-music-control中有safeExtGet获取版本号 + //以下代码用于统一项目编译版本 buildToolsVersion = "28.0.3" - minSdkVersion = 16 - compileSdkVersion = 28 - targetSdkVersion = 28 + minSdkVersion = 21 + compileSdkVersion = 32 + targetSdkVersion = 33 supportLibVersion = "28.0.0" + set('compileSdkVersion', compileSdkVersion) + set('buildToolsVersion', buildToolsVersion) + set('minSdkVersion', minSdkVersion) + set('targetSdkVersion', targetSdkVersion) } repositories { google() jcenter() } dependencies { - classpath("com.android.tools.build:gradle:3.4.0") + classpath "com.android.tools.build:gradle:3.4.3" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } +def REACT_NATIVE_VERSION = new File(['node', '--print', "JSON.parse(require('fs').readFileSync(require.resolve('react-native/package.json'), 'utf-8')).version"].execute(null, rootDir).text.trim()) allprojects { repositories { @@ -29,5 +36,17 @@ allprojects { // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm url "$rootDir/../node_modules/react-native/android" } + maven { + // Android JSC is installed from npm + url("$rootDir/../node_modules/jsc-android/dist") + } + } + configurations.all { + //低于0.65版本的‘react-native’需要加上这段代码, + //否则会出现类似:程序包com.facebook.react.common不存在 的错误 + resolutionStrategy { + // Remove this override in 0.65+, as a proper fix is included in react-native itself. + force "com.facebook.react:react-native:" + REACT_NATIVE_VERSION + } } } diff --git a/android/gradle.properties b/android/gradle.properties index 2adc8e6..fa157e1 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -10,7 +10,7 @@ # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. # Default value: -Xmx10248m -XX:MaxPermSize=256m -# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit @@ -19,4 +19,6 @@ MYAPP_UPLOAD_STORE_FILE=my-release-key.keystore MYAPP_UPLOAD_KEY_ALIAS=my-key-alias MYAPP_UPLOAD_STORE_PASSWORD=lovemusic -MYAPP_UPLOAD_KEY_PASSWORD=lovemusic \ No newline at end of file +MYAPP_UPLOAD_KEY_PASSWORD=lovemusic +android.useAndroidX=true +android.enableJetifier=true \ No newline at end of file diff --git a/android/settings.gradle b/android/settings.gradle index ef6c9bc..d507178 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,4 +1,10 @@ rootProject.name = 'Listen1' +include ':react-native-vector-icons' +project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android') +include ':react-native-screens' +project(':react-native-screens').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-screens/android') +include ':@react-native-community_toolbar-android' +project(':@react-native-community_toolbar-android').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/toolbar-android/android') include ':@react-native-community_async-storage' project(':@react-native-community_async-storage').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/async-storage/android') include ':react-native-music-control' diff --git a/package.json b/package.json index 07e8af5..7560a0d 100755 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@react-native-community/async-storage": "^1.12.1", "@react-native-community/toolbar-android": "^0.1.0-rc.2", "deprecated-react-native-listview": "^0.0.5", + "jetifier": "^2.0.0", "md5": "^2.2.1", "query-string": "^6.8.1", "react": "16.8.3", diff --git a/src/views/setting/export-local.screen.js b/src/views/setting/export-local.screen.js index a8af577..7048de1 100644 --- a/src/views/setting/export-local.screen.js +++ b/src/views/setting/export-local.screen.js @@ -1,5 +1,6 @@ import React from 'react'; -import { ScrollView, Clipboard, Button } from 'react-native'; +import { ScrollView, Clipboard, View, Platform, NativeModules} from 'react-native'; +import { Button } from 'react-native-elements'; import { connect } from 'react-redux'; import styled, { withTheme } from 'styled-components'; import StateJsonConvert from '../../modules/state-json-convert'; @@ -25,8 +26,9 @@ class ExportLocal extends React.Component { }; constructor(props) { super(props); - this.state = { jsonString: '' }; + this.state = { jsonString: '' , deviceOS: Platform.OS}; this.onPressCopy = this.onPressCopy.bind(this); + this.onExportConfigToFile = this.onExportConfigToFile.bind(this); } componentDidMount() { // generate backup json @@ -39,8 +41,18 @@ class ExportLocal extends React.Component { } onPressCopy = () => { Clipboard.setString(this.state.jsonString); + console.log(`render ${this.state.jsonString}`); showToast('已复制到剪切板'); }; + onExportConfigToFile=()=>{ + NativeModules.FileImportConfig.writeFile(this.state.jsonString,(writeState)=>{ + if (writeState) { + showToast('备份成功: /storage/emulated/0/Download/listen1_backup.json'); + }else{ + showToast('备份失败'); + } + }) + }; render() { // console.log(`render ${this.constructor.name}`); @@ -50,6 +62,11 @@ class ExportLocal extends React.Component { {this.state.jsonString}