diff --git a/app/build.gradle b/app/build.gradle index b75acb596..9ba6363ee 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -76,6 +76,7 @@ dependencies { implementation 'com.google.android.material:material:1.7.0' implementation 'com.google.code.gson:gson:2.10' implementation 'com.google.zxing:core:3.5.1' + implementation 'com.guolindev.permissionx:permissionx:1.7.1' implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.10' implementation 'me.jessyan:autosize:1.2.1' implementation 'org.greenrobot:eventbus:3.3.1' diff --git a/app/src/leanback/java/com/fongmi/android/tv/ui/activity/SearchActivity.java b/app/src/leanback/java/com/fongmi/android/tv/ui/activity/SearchActivity.java index 981343413..325340651 100644 --- a/app/src/leanback/java/com/fongmi/android/tv/ui/activity/SearchActivity.java +++ b/app/src/leanback/java/com/fongmi/android/tv/ui/activity/SearchActivity.java @@ -8,9 +8,6 @@ import android.view.KeyEvent; import android.view.View; import android.view.inputmethod.EditorInfo; -import androidx.activity.result.ActivityResultCallback; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.viewbinding.ViewBinding; @@ -45,13 +42,6 @@ public class SearchActivity extends BaseActivity implements WordAdapter.OnClickL activity.startActivity(new Intent(activity, SearchActivity.class)); } - private final ActivityResultLauncher launcher = registerForActivityResult(new ActivityResultContracts.RequestPermission(), new ActivityResultCallback<>() { - @Override - public void onActivityResult(Boolean isGranted) { - if (isGranted) mBinding.mic.start(); - } - }); - @Override protected ViewBinding getBinding() { return mBinding = ActivitySearchBinding.inflate(getLayoutInflater()); @@ -77,7 +67,7 @@ public class SearchActivity extends BaseActivity implements WordAdapter.OnClickL else getSuggest(s.toString()); } }); - mBinding.mic.setListener(launcher, new CustomListener() { + mBinding.mic.setListener(this, new CustomListener() { @Override public void onEndOfSpeech() { mBinding.mic.stop(); diff --git a/app/src/leanback/java/com/fongmi/android/tv/ui/activity/SettingActivity.java b/app/src/leanback/java/com/fongmi/android/tv/ui/activity/SettingActivity.java index 99d73a57b..16da9acc7 100644 --- a/app/src/leanback/java/com/fongmi/android/tv/ui/activity/SettingActivity.java +++ b/app/src/leanback/java/com/fongmi/android/tv/ui/activity/SettingActivity.java @@ -19,12 +19,14 @@ import com.fongmi.android.tv.BuildConfig; import com.fongmi.android.tv.R; import com.fongmi.android.tv.api.ApiConfig; import com.fongmi.android.tv.api.LiveConfig; +import com.fongmi.android.tv.api.Updater; import com.fongmi.android.tv.api.WallConfig; import com.fongmi.android.tv.bean.Config; import com.fongmi.android.tv.bean.Live; import com.fongmi.android.tv.bean.Site; import com.fongmi.android.tv.databinding.ActivitySettingBinding; import com.fongmi.android.tv.event.RefreshEvent; +import com.fongmi.android.tv.event.ServerEvent; import com.fongmi.android.tv.impl.ConfigCallback; import com.fongmi.android.tv.impl.LiveCallback; import com.fongmi.android.tv.impl.SiteCallback; @@ -36,13 +38,15 @@ import com.fongmi.android.tv.ui.custom.dialog.SiteDialog; import com.fongmi.android.tv.utils.Notify; import com.fongmi.android.tv.utils.Prefers; import com.fongmi.android.tv.utils.ResUtil; -import com.fongmi.android.tv.api.Updater; +import com.fongmi.android.tv.utils.Utils; +import com.permissionx.guolindev.PermissionX; + +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; public class SettingActivity extends BaseActivity implements ConfigCallback, SiteCallback, LiveCallback { - private final ActivityResultLauncher launcherString = registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> loadConfig()); private final ActivityResultLauncher launcherIntent = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> loadConfig()); - private ActivitySettingBinding mBinding; private Config config; @@ -96,10 +100,10 @@ public class SettingActivity extends BaseActivity implements ConfigCallback, Sit } private void checkPermission() { - if (config.getUrl().startsWith("file") && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) { openSetting(); - } else if (config.getUrl().startsWith("file") && Build.VERSION.SDK_INT < Build.VERSION_CODES.R && ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - launcherString.launch(Manifest.permission.READ_EXTERNAL_STORAGE); + } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R && ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + PermissionX.init(this).permissions(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE).request((allGranted, grantedList, deniedList) -> loadConfig()); } else { loadConfig(); } @@ -167,6 +171,11 @@ public class SettingActivity extends BaseActivity implements ConfigCallback, Sit } } + @Subscribe(threadMode = ThreadMode.MAIN) + public void onServerEvent(ServerEvent event) { + if (event.getType() == ServerEvent.Type.FILE) Utils.checkStoragePermission(this); + } + @Override public void setSite(Site item) { ApiConfig.get().setHome(item); diff --git a/app/src/leanback/java/com/fongmi/android/tv/ui/custom/CustomMic.java b/app/src/leanback/java/com/fongmi/android/tv/ui/custom/CustomMic.java index a5c0c91c1..3ffb2bcf0 100644 --- a/app/src/leanback/java/com/fongmi/android/tv/ui/custom/CustomMic.java +++ b/app/src/leanback/java/com/fongmi/android/tv/ui/custom/CustomMic.java @@ -3,7 +3,6 @@ package com.fongmi.android.tv.ui.custom; import android.Manifest; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; import android.graphics.PorterDuff; import android.graphics.Rect; import android.speech.RecognizerIntent; @@ -12,21 +11,21 @@ import android.util.AttributeSet; import android.view.KeyEvent; import android.view.animation.Animation; -import androidx.activity.result.ActivityResultLauncher; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatImageView; -import androidx.core.content.ContextCompat; +import androidx.fragment.app.FragmentActivity; import com.fongmi.android.tv.R; import com.fongmi.android.tv.utils.ResUtil; import com.fongmi.android.tv.utils.Utils; import com.github.bassaer.library.MDColor; +import com.permissionx.guolindev.PermissionX; public class CustomMic extends AppCompatImageView { - private ActivityResultLauncher launcher; private SpeechRecognizer recognizer; + private FragmentActivity activity; private Animation flicker; private boolean listen; @@ -44,10 +43,6 @@ public class CustomMic extends AppCompatImageView { recognizer = SpeechRecognizer.createSpeechRecognizer(context); } - private boolean hasPermission() { - return ContextCompat.checkSelfPermission(getContext(), Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED; - } - private boolean isListen() { return listen; } @@ -56,14 +51,15 @@ public class CustomMic extends AppCompatImageView { this.listen = listen; } - public void setListener(ActivityResultLauncher launcher, CustomListener listener) { + public void setListener(FragmentActivity activity, CustomListener listener) { this.recognizer.setRecognitionListener(listener); - this.launcher = launcher; + this.activity = activity; } - private void check() { - if (hasPermission()) start(); - else launcher.launch(Manifest.permission.RECORD_AUDIO); + private void checkPermission() { + PermissionX.init(activity).permissions(Manifest.permission.RECORD_AUDIO).request((allGranted, grantedList, deniedList) -> { + if (allGranted) start(); + }); } private void startListening() { @@ -93,7 +89,7 @@ public class CustomMic extends AppCompatImageView { @Override protected void onFocusChanged(boolean gainFocus, int direction, @Nullable Rect previouslyFocusedRect) { super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); - if (gainFocus) check(); + if (gainFocus) checkPermission(); else stop(); } diff --git a/app/src/leanback/java/com/fongmi/android/tv/ui/custom/dialog/ConfigDialog.java b/app/src/leanback/java/com/fongmi/android/tv/ui/custom/dialog/ConfigDialog.java index c6a8e5e86..b3fecc1ea 100644 --- a/app/src/leanback/java/com/fongmi/android/tv/ui/custom/dialog/ConfigDialog.java +++ b/app/src/leanback/java/com/fongmi/android/tv/ui/custom/dialog/ConfigDialog.java @@ -98,8 +98,8 @@ public class ConfigDialog implements DialogInterface.OnDismissListener { private void onPositive(View view) { String text = Utils.checkClan(binding.text.getText().toString().trim()); - if (!url.equals(text)) callback.setConfig(Config.find(text, type)); if (text.isEmpty()) Config.delete(url, type); + callback.setConfig(Config.find(text, type)); dialog.dismiss(); } diff --git a/app/src/main/java/com/fongmi/android/tv/event/ServerEvent.java b/app/src/main/java/com/fongmi/android/tv/event/ServerEvent.java index 11f4aad05..b81ac7cb4 100644 --- a/app/src/main/java/com/fongmi/android/tv/event/ServerEvent.java +++ b/app/src/main/java/com/fongmi/android/tv/event/ServerEvent.java @@ -4,8 +4,8 @@ import org.greenrobot.eventbus.EventBus; public class ServerEvent { - private final String text; private final Type type; + private String text; public static void search(String text) { EventBus.getDefault().post(new ServerEvent(Type.SEARCH, text)); @@ -23,6 +23,14 @@ public class ServerEvent { EventBus.getDefault().post(new ServerEvent(Type.API, text)); } + public static void file() { + EventBus.getDefault().post(new ServerEvent(Type.FILE)); + } + + public ServerEvent(Type type) { + this.type = type; + } + private ServerEvent(Type type, String text) { this.type = type; this.text = text; @@ -37,6 +45,6 @@ public class ServerEvent { } public enum Type { - SEARCH, UPDATE, PUSH, API + SEARCH, UPDATE, PUSH, API, FILE } } diff --git a/app/src/main/java/com/fongmi/android/tv/server/Nano.java b/app/src/main/java/com/fongmi/android/tv/server/Nano.java index 9998a2093..05692d453 100644 --- a/app/src/main/java/com/fongmi/android/tv/server/Nano.java +++ b/app/src/main/java/com/fongmi/android/tv/server/Nano.java @@ -2,6 +2,7 @@ package com.fongmi.android.tv.server; import com.fongmi.android.tv.R; import com.fongmi.android.tv.api.ApiConfig; +import com.fongmi.android.tv.event.ServerEvent; import com.fongmi.android.tv.server.process.InputRequestProcess; import com.fongmi.android.tv.server.process.RawRequestProcess; import com.fongmi.android.tv.server.process.RequestProcess; @@ -11,7 +12,6 @@ import com.google.gson.JsonObject; import java.io.File; import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.InputStream; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -101,7 +101,7 @@ public class Nano extends NanoHTTPD { File file = FileUtil.getRootFile(path); if (file.isFile()) return newChunkedResponse(Response.Status.OK, "application/octet-stream", new FileInputStream(file)); else return newFixedLengthResponse(Response.Status.OK, MIME_PLAINTEXT, listFiles(file)); - } catch (FileNotFoundException e) { + } catch (Exception e) { return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, e.getMessage()); } } @@ -150,6 +150,7 @@ public class Nano extends NanoHTTPD { JsonObject info = new JsonObject(); info.addProperty("parent", parent); if (list == null || list.length == 0) { + if (parent.equals(".")) ServerEvent.file(); info.add("files", new JsonArray()); return info.toString(); } diff --git a/app/src/main/java/com/fongmi/android/tv/utils/Utils.java b/app/src/main/java/com/fongmi/android/tv/utils/Utils.java index cb9c29a56..ab593cce2 100644 --- a/app/src/main/java/com/fongmi/android/tv/utils/Utils.java +++ b/app/src/main/java/com/fongmi/android/tv/utils/Utils.java @@ -1,8 +1,13 @@ package com.fongmi.android.tv.utils; +import android.Manifest; import android.app.Activity; import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; import android.net.Uri; +import android.os.Build; +import android.os.Environment; import android.os.IBinder; import android.provider.Settings; import android.text.TextUtils; @@ -11,9 +16,15 @@ import android.view.KeyEvent; import android.view.View; import android.view.inputmethod.InputMethodManager; +import androidx.annotation.RequiresApi; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.FragmentActivity; + import com.fongmi.android.tv.App; +import com.fongmi.android.tv.BuildConfig; import com.fongmi.android.tv.server.Server; import com.google.android.exoplayer2.util.Util; +import com.permissionx.guolindev.PermissionX; import java.math.BigInteger; import java.security.MessageDigest; @@ -166,4 +177,21 @@ public class Utils { imm.hideSoftInputFromWindow(windowToken, 0); } } + + public static void checkStoragePermission(FragmentActivity activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) { + Utils.openManageFileAccess(); + } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R && ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + PermissionX.init(activity).permissions(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE).request(null); + } + } + + @RequiresApi(api = Build.VERSION_CODES.R) + public static void openManageFileAccess() { + try { + App.get().startActivity(new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, Uri.parse("package:" + BuildConfig.APPLICATION_ID))); + } catch (Exception e) { + App.get().startActivity(new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)); + } + } } diff --git a/app/src/main/res/raw/script.js b/app/src/main/res/raw/script.js index 39641a427..047fee0b7 100644 --- a/app/src/main/res/raw/script.js +++ b/app/src/main/res/raw/script.js @@ -21,6 +21,10 @@ function api() { doAction('api', { text: $('#api_url').val() }); } +function api(text) { + doAction('api', { text: text }); +} + function doAction(action, kv) { kv['do'] = action; $.post('/action', kv, function (data) { @@ -79,7 +83,7 @@ function selectFile(path, canDel) { } function fileToApi() { - doAction('api', { text: "file://" + current_file }); + api("file://" + current_file); hideFileInfo(); }