diff --git a/__tests__/mediafetch/acfunVideo.test.ts b/__tests__/mediafetch/acfunVideo.test.ts new file mode 100644 index 00000000..555d960f --- /dev/null +++ b/__tests__/mediafetch/acfunVideo.test.ts @@ -0,0 +1,10 @@ +import fetcher from '../../src/utils/mediafetch/acfunvideo'; + +test('acfunvideo', async () => { + const content = await fetcher.regexFetch({ + reExtracted: fetcher.regexSearchMatch.exec( + 'https://www.acfun.cn/v/ac46370925' + )!, + }); + expect(content?.songList[0]?.id).not.toBeUndefined(); +}); diff --git a/android/app/build.gradle b/android/app/build.gradle index 1d9db061..f7f180e3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -157,6 +157,7 @@ android { buildFeatures { buildConfig = true + viewBinding true } compileOptions { @@ -184,6 +185,7 @@ dependencies { // The version of react-native is set by the React Native Gradle Plugin implementation("com.facebook.react:react-android") + implementation("androidx.media3:media3-session:1.4.1") if (hermesEnabled.toBoolean()) { implementation("com.facebook.react:hermes-android") diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index fafd6278..76fa626b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,86 +1,112 @@ - + + - + - - - - - + + + - - - - - - + android:usesCleartextTraffic="false" + tools:replace="android:allowBackup"> + + + + + + + + + + + + + + - + android:name="androidx.core.content.FileProvider" + android:authorities="com.noxplay.noxplayer.provider" + android:exported="false" + android:grantUriPermissions="true"> + - + android:resizeableActivity="true" + android:supportsPictureInPicture="true" + android:windowSoftInputMode="adjustResize"> + + + + - - + + + - - + + + android:exported="true" + tools:node="replace"> - - - - - + + + + + + + - + + diff --git a/android/app/src/main/java/com/noxplay/noxplayer/APMWidget.kt b/android/app/src/main/java/com/noxplay/noxplayer/APMWidget.kt new file mode 100644 index 00000000..79b0b77f --- /dev/null +++ b/android/app/src/main/java/com/noxplay/noxplayer/APMWidget.kt @@ -0,0 +1,155 @@ +package com.noxplay.noxplayer + +import android.app.PendingIntent +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.util.Log +import android.widget.RemoteViews +import com.doublesymmetry.trackplayer.model.Track +import com.doublesymmetry.trackplayer.module.MusicEvents +import com.doublesymmetry.trackplayer.service.MusicService +import com.lovegaoshi.kotlinaudio.models.AudioPlayerState + +/** + * Implementation of App Widget functionality. + */ +class APMWidget : AppWidgetProvider() { + + private lateinit var binder: MusicService.MusicBinder + private var currentTrack: Track? = null + + private fun bindService (context: Context?): Boolean { + if (!::binder.isInitialized || !binder.isBinderAlive) { + if (context == null) return false + Log.d("APM", "widget attempt to connect to MusicService...") + val mBinder = peekService(context, Intent(context, MusicService::class.java)) ?: return false + Log.d("APM", "widget attempt to cast to MusicService...") + binder = mBinder as MusicService.MusicBinder + if (!binder.isBinderAlive) return false + } + return true + } + + private fun emit(context: Context?, e: String) { + if (!bindService(context)) return + binder.service.emit(e) + } + + private fun initWidget(views: RemoteViews, context: Context): RemoteViews { + views.setOnClickPendingIntent( + R.id.buttonPrev, PendingIntent.getBroadcast( + context, 0, + Intent(context, APMWidget::class.java).setAction(MusicEvents.BUTTON_SKIP_PREVIOUS), + PendingIntent.FLAG_IMMUTABLE)) + views.setOnClickPendingIntent( + R.id.buttonPlay, PendingIntent.getBroadcast( + context, 0, + Intent(context, APMWidget::class.java).setAction(MusicEvents.BUTTON_PLAY_PAUSE), + PendingIntent.FLAG_IMMUTABLE)) + views.setOnClickPendingIntent( + R.id.buttonNext, PendingIntent.getBroadcast( + context, 0, + Intent(context, APMWidget::class.java).setAction(MusicEvents.BUTTON_SKIP_NEXT), + PendingIntent.FLAG_IMMUTABLE)) + views.setOnClickPendingIntent( + R.id.APMWidget, PendingIntent.getBroadcast( + context, 0, + Intent(context, APMWidget::class.java).setAction(WIDGET_CLICK), + PendingIntent.FLAG_IMMUTABLE)) + return views + } + + private fun updateTrack(views: RemoteViews, track: Track?, bitmap: Bitmap?): RemoteViews { + if (track == null) { + views.setTextViewCompoundDrawables( + R.id.buttonPlay, R.drawable.media3_icon_play,0,0,0) + } + views.setTextViewText(R.id.songName, track?.title ?: "") + views.setTextViewText(R.id.artistName, track?.artist ?: "") + views.setImageViewBitmap(R.id.albumArt, bitmap) + return views + } + + private fun updatePlayPause(views: RemoteViews) { + val isPlaying = binder.service.state === AudioPlayerState.PLAYING + views.setTextViewCompoundDrawables( + R.id.buttonPlay, + if (isPlaying) R.drawable.media3_icon_pause else R.drawable.media3_icon_play, + 0,0,0) + } + + // HACK: properly abstract all of these + private fun clearWidgetContent(context: Context?) { + if (context == null) return + val widgetManager = AppWidgetManager.getInstance(context) + val ids = widgetManager.getAppWidgetIds(ComponentName(context, APMWidget::class.java)) + val views = RemoteViews(context.packageName, R.layout.a_p_m_widget) + updateTrack(views, null, null) + ids.forEach { id -> widgetManager.updateAppWidget(id, views) } + // onUpdate(context, widgetManager, ids) + } + + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray + ) { + // Construct the RemoteViews object + val views = RemoteViews(context.packageName, R.layout.a_p_m_widget) + if (!bindService(context)) { + updateTrack(views, null, null) + } else { + initWidget(views, context) + updatePlayPause(views) + val track = binder.service.currentTrack + if (track != currentTrack) { + currentTrack = track + val bitmap = if (binder.service.currentBitmap.size == 1) binder.service.currentBitmap[0] else null + updateTrack(views, currentTrack, bitmap) + } + } + // Instruct the widget manager to update the widget + for (appWidgetId in appWidgetIds) { + appWidgetManager.updateAppWidget(appWidgetId, views) + } + } + + override fun onReceive(context: Context?, intent: Intent?) { + try { + when (intent?.action) { + "clear-widget" -> clearWidgetContent(context) + WIDGET_CLICK -> { + if (!bindService(context)) { + Intent(context, MainActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + }.also {i -> context?.startActivity(i)} + return + } + } + MusicEvents.BUTTON_SKIP_PREVIOUS -> emit(context, MusicEvents.BUTTON_SKIP_PREVIOUS) + MusicEvents.BUTTON_SKIP_NEXT -> emit(context, MusicEvents.BUTTON_SKIP_NEXT) + MusicEvents.BUTTON_PLAY_PAUSE -> emit(context, MusicEvents.BUTTON_PLAY_PAUSE) + else -> {} + } + } catch (e: Exception) { + Log.w("APM", "widget action ${intent?.action} failed by $e") + + } + super.onReceive(context, intent) + } + + override fun onEnabled(context: Context) { + // Enter relevant functionality for when the first widget is created + } + + override fun onDisabled(context: Context) { + // Enter relevant functionality for when the last widget is disabled + } +} + +const val WIDGET_CLICK = "widget-click" +const val WIDGET_CLEAR = "clear-widget" \ No newline at end of file diff --git a/android/app/src/main/java/com/noxplay/noxplayer/APMWidgetModule.kt b/android/app/src/main/java/com/noxplay/noxplayer/APMWidgetModule.kt new file mode 100644 index 00000000..cb35f2e7 --- /dev/null +++ b/android/app/src/main/java/com/noxplay/noxplayer/APMWidgetModule.kt @@ -0,0 +1,24 @@ +package com.noxplay.noxplayer + +import android.appwidget.AppWidgetManager +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.util.Log +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod + +class APMWidgetModule (private val reactContext: ReactApplicationContext) : + ReactContextBaseJavaModule(reactContext) { + override fun getName() = "APMWidgetModule" + + @ReactMethod fun updateWidget() { + val intent = Intent(reactContext, APMWidget::class.java) + intent.setAction("android.appwidget.action.APPWIDGET_UPDATE") + val widgetManager = AppWidgetManager.getInstance(reactContext) + val ids = widgetManager.getAppWidgetIds(ComponentName(reactContext, APMWidget::class.java)) + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids) + reactContext.sendBroadcast(intent) + } +} diff --git a/android/app/src/main/java/com/noxplay/noxplayer/MainActivity.kt b/android/app/src/main/java/com/noxplay/noxplayer/MainActivity.kt index b64306a4..327ae265 100644 --- a/android/app/src/main/java/com/noxplay/noxplayer/MainActivity.kt +++ b/android/app/src/main/java/com/noxplay/noxplayer/MainActivity.kt @@ -1,19 +1,22 @@ package com.noxplay.noxplayer import android.annotation.SuppressLint +import android.app.AlarmManager +import android.app.PendingIntent import android.app.PictureInPictureParams import android.content.ComponentCallbacks2 +import android.content.Context import android.content.Intent import android.content.res.Configuration import android.os.Build import android.os.Bundle import android.os.Debug +import android.os.SystemClock import android.util.Rational import com.facebook.react.ReactActivity import com.facebook.react.ReactActivityDelegate import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled import com.facebook.react.defaults.DefaultReactActivityDelegate -import com.facebook.react.modules.core.DeviceEventManagerModule import com.facebook.react.bridge.Arguments import expo.modules.ReactActivityDelegateWrapper import timber.log.Timber @@ -24,33 +27,29 @@ class MainActivity : ReactActivity(), ComponentCallbacks2 { */ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(null) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { - setShowWhenLocked(true) - setTurnScreenOn(true) - } if ("trackplayer://service-bound" in intent.data.toString()) { moveTaskToBack(true) } } - @SuppressLint("VisibleForTests") - override fun onNewIntent(intent: Intent) { - super.onNewIntent(intent) - try { - if (intent.action?.contains("android.media.action.MEDIA_PLAY_FROM_SEARCH") == true) { - this.reactInstanceManager.currentReactContext - ?.emitDeviceEvent("remote-play-search", Arguments.fromBundle(intent.extras ?: Bundle())) - } - val launchOptions = Bundle() - launchOptions.putString("intentData", intent.dataString) - launchOptions.putString("intentAction", intent.action) - launchOptions.putBundle("intentBundle", intent.extras ?: Bundle()) - this.reactInstanceManager.currentReactContext - ?.emitDeviceEvent("APMNewIntent", Arguments.fromBundle(launchOptions)) - } catch (e: Exception) { - Timber.tag("APM-intent").d("failed to notify intent: $intent") - } - } + @SuppressLint("VisibleForTests") + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + try { + if (intent.action?.contains("android.media.action.MEDIA_PLAY_FROM_SEARCH") == true) { + this.reactInstanceManager.currentReactContext + ?.emitDeviceEvent("remote-play-search", Arguments.fromBundle(intent.extras ?: Bundle())) + } + val launchOptions = Bundle() + launchOptions.putString("intentData", intent.dataString) + launchOptions.putString("intentAction", intent.action) + launchOptions.putBundle("intentBundle", intent.extras ?: Bundle()) + this.reactInstanceManager.currentReactContext + ?.emitDeviceEvent("APMNewIntent", Arguments.fromBundle(launchOptions)) + } catch (e: Exception) { + Timber.tag("APM-intent").d("failed to notify intent: $intent") + } + } /** * Returns the name of the main component registered from JavaScript. This is used to schedule * rendering of the component. @@ -61,20 +60,20 @@ class MainActivity : ReactActivity(), ComponentCallbacks2 { class APMReactActivityDelegate(activity: ReactActivity, componentName: String): DefaultReactActivityDelegate(activity, componentName, fabricEnabled) { - private val mActivity = activity + private val mActivity = activity - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } - override fun getLaunchOptions(): Bundle { - val launchOptions = super.getLaunchOptions() ?: Bundle() - launchOptions.putString("intentData", mActivity.intent.dataString) - launchOptions.putString("intentAction", mActivity.intent.action ?: "") - // launchOptions.putBundle("intentBundle", mActivity.intent.extras ?: Bundle()) - return launchOptions - } + override fun getLaunchOptions(): Bundle { + val launchOptions = super.getLaunchOptions() ?: Bundle() + launchOptions.putString("intentData", mActivity.intent.dataString) + launchOptions.putString("intentAction", mActivity.intent.action ?: "") + // launchOptions.putBundle("intentBundle", mActivity.intent.extras ?: Bundle()) + return launchOptions } + } /** * Returns the instance of the [ReactActivityDelegate]. Here we use a util class [ ] which allows you to easily enable Fabric and Concurrent React @@ -83,9 +82,9 @@ class MainActivity : ReactActivity(), ComponentCallbacks2 { override fun createReactActivityDelegate(): ReactActivityDelegate { return ReactActivityDelegateWrapper( this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, APMReactActivityDelegate( - this, - mainComponentName, // If you opted-in for the New Architecture, we enable the Fabric Renderer. - ) + this, + mainComponentName, // If you opted-in for the New Architecture, we enable the Fabric Renderer. + ) ) } @@ -124,23 +123,36 @@ class MainActivity : ReactActivity(), ComponentCallbacks2 { super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) } - private fun logAPMRAM() { - val nativeHeapSize = Debug.getNativeHeapSize() - val nativeHeapFreeSize = Debug.getNativeHeapFreeSize() - val usedMemInBytes = nativeHeapSize - nativeHeapFreeSize - val usedMemInPercentage = usedMemInBytes * 100 / nativeHeapSize - Timber.tag("APMRAM").d("APM RAM usage: $usedMemInBytes/1000/1000 ($usedMemInPercentage)") - } + private fun logAPMRAM() { + val nativeHeapSize = Debug.getNativeHeapSize() + val nativeHeapFreeSize = Debug.getNativeHeapFreeSize() + val usedMemInBytes = nativeHeapSize - nativeHeapFreeSize + val usedMemInPercentage = usedMemInBytes * 100 / nativeHeapSize + Timber.tag("APMRAM").d("APM RAM usage: $usedMemInBytes/1000/1000 ($usedMemInPercentage)") + } + + override fun onTrimMemory(level: Int) { + Timber.tag("APMRAMTrim").d("trim memory level $level emitted.") + logAPMRAM() + super.onTrimMemory(level) + } - override fun onTrimMemory(level: Int) { - Timber.tag("APMRAMTrim").d("trim memory level $level emitted.") - logAPMRAM() - super.onTrimMemory(level) - } + override fun onLowMemory() { + Timber.tag("APMRAMLow").d("low system memory emitted.") + logAPMRAM() + super.onLowMemory() + } - override fun onLowMemory() { - Timber.tag("APMRAMLow").d("low system memory emitted.") - logAPMRAM() - super.onLowMemory() - } + override fun onDestroy() { + // https://stackoverflow.com/questions/5764099/how-to-update-a-widget-if-the-related-service-gets-killed + val am = getSystemService(Context.ALARM_SERVICE) as AlarmManager + val intent = Intent(this, APMWidget::class.java) + intent.setAction(WIDGET_CLEAR) + am.set( + AlarmManager.ELAPSED_REALTIME, + SystemClock.elapsedRealtime(), + PendingIntent.getBroadcast(this, 5424, intent, PendingIntent.FLAG_IMMUTABLE) + ) + super.onDestroy() + } } diff --git a/android/app/src/main/java/com/noxplay/noxplayer/NoxAndroidAutoModule.kt b/android/app/src/main/java/com/noxplay/noxplayer/NoxAndroidAutoModule.kt deleted file mode 100644 index 426bfa7d..00000000 --- a/android/app/src/main/java/com/noxplay/noxplayer/NoxAndroidAutoModule.kt +++ /dev/null @@ -1,203 +0,0 @@ -package com.noxplay.noxplayer - -import android.annotation.SuppressLint -import android.app.ActivityManager -import android.app.ApplicationExitInfo -import android.content.ContentUris -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.os.Build -import android.provider.MediaStore -import android.provider.Settings -import android.view.WindowManager -import androidx.core.content.FileProvider -import com.lovegaoshi.kotlinaudio.utils.bitmapCoverDir -import com.lovegaoshi.kotlinaudio.utils.bitmapCoverFileName -import com.facebook.react.bridge.Arguments -import com.facebook.react.bridge.Promise -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.bridge.ReactContextBaseJavaModule -import com.facebook.react.bridge.ReactMethod -import com.facebook.react.bridge.WritableArray -import com.facebook.react.bridge.WritableNativeArray -import timber.log.Timber -import java.io.File - - -class NoxAndroidAutoModule(reactContext: ReactApplicationContext) : - ReactContextBaseJavaModule(reactContext) { - override fun getName() = "NoxAndroidAutoModule" - - private fun listMediaDirNative(relativeDir: String, subdir: Boolean, selection: String? = null): WritableArray { - val results: WritableArray = WritableNativeArray() - try { - val query = reactApplicationContext.contentResolver.query( - MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, - arrayOf( - MediaStore.Audio.Media._ID, - MediaStore.Audio.Media.RELATIVE_PATH, - MediaStore.Audio.Media.DISPLAY_NAME, - MediaStore.Audio.Media.DATA, - MediaStore.Audio.Media.TITLE, - MediaStore.Audio.Media.ALBUM, - MediaStore.Audio.Media.ARTIST, - MediaStore.Audio.Media.BITRATE, - MediaStore.Audio.Media.DURATION, - ), selection,null, null) - query?.use { cursor -> - val pathColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.RELATIVE_PATH) - while (cursor.moveToNext()) { - val mediaPath = cursor.getString(pathColumn) - if (mediaPath == relativeDir || (subdir && mediaPath.startsWith(relativeDir))) { - val mediaItem = Arguments.createMap() - mediaItem.putString("URI", - "content:/" + ContentUris.appendId( - Uri.Builder().path(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.path), - cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID))).build().toString()) - mediaItem.putString("relativePath",mediaPath) - mediaItem.putString("fileName", - cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME)) - ) - mediaItem.putString("realPath", - cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA)) - ) - mediaItem.putString("title", - cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE)) - ) - mediaItem.putString("album", - cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM)) - ) - mediaItem.putString("artist", - cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST)) - ) - mediaItem.putInt("duration", - cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)) - ) - mediaItem.putInt("bitrate", - cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.BITRATE)) - ) - results.pushMap(mediaItem) - } - } - } - } catch (e: Exception) { - Timber.tag("NoxFileUtil").e(e.toString()) - } - return results - } - - @ReactMethod fun getUri(uri: String, callback: Promise) { - callback.resolve(FileProvider.getUriForFile(reactApplicationContext, - "${BuildConfig.APPLICATION_ID}.provider", File(uri)).toString()) - } - - @ReactMethod fun listMediaDir(relativeDir: String, subdir: Boolean, callback: Promise) { - callback.resolve(listMediaDirNative(relativeDir, subdir)) - } - - @ReactMethod fun listMediaFileByFName(filename: String, relDir: String, callback: Promise) { - callback.resolve(listMediaDirNative(relDir, true, - "${MediaStore.Audio.Media.DISPLAY_NAME} IN ('$filename')")) - } - - @ReactMethod fun listMediaFileByID(id: String, callback: Promise) { - callback.resolve(listMediaDirNative("", true, - "${MediaStore.Audio.Media._ID} = $id")) - } - @ReactMethod fun getLastExitReason(callback: Promise) { - try { - val activity = reactApplicationContext.currentActivity - val am = activity?.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - val reason = am.getHistoricalProcessExitReasons( - "com.noxplay.noxplayer",0,0 - )[0].reason - callback.resolve(reason in intArrayOf( - ApplicationExitInfo.REASON_USER_REQUESTED, - ApplicationExitInfo.REASON_USER_STOPPED, - ApplicationExitInfo.REASON_EXIT_SELF, - ApplicationExitInfo.REASON_PERMISSION_CHANGE, - ApplicationExitInfo.REASON_PACKAGE_UPDATED, - ApplicationExitInfo.REASON_EXCESSIVE_RESOURCE_USAGE, - ApplicationExitInfo.REASON_LOW_MEMORY, - ApplicationExitInfo.REASON_SIGNALED, - )) - } else { - callback.resolve(true) - } - } catch (e: Exception) { - callback.resolve(true) - } - } - - @ReactMethod fun disableShowWhenLocked() { - val activity = reactApplicationContext.currentActivity - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { - activity?.setShowWhenLocked(false) - activity?.setTurnScreenOn(false) - } - } - @ReactMethod fun getDrawOverAppsPermission(callback: Promise) { - val context = reactApplicationContext - val activity = context.currentActivity - try { - val canDraw = Settings.canDrawOverlays(activity) - callback.resolve(canDraw) - } catch (e: Exception) { - callback.resolve(false) - } - } - - @ReactMethod fun askDrawOverAppsPermission() { - val context = reactApplicationContext - val intent = Intent( - Settings.ACTION_MANAGE_OVERLAY_PERMISSION, - Uri.parse("package:com.noxplay.noxplayer") - ) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - context.startActivity(intent) - } - - @ReactMethod fun keepScreenOn(screenOn: Boolean = true) { - val context = reactApplicationContext - val activity = context.currentActivity - val window = activity?.window - if (screenOn) { - window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - } else { - window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - } - } - - @ReactMethod fun isGestureNavigationMode(callback: Promise) { - val context = reactApplicationContext - callback.resolve( - Settings.Secure.getInt(context.contentResolver, "navigation_mode", 0) == 2 - ) - } - - @SuppressLint("Range") - private fun getAPMCacheUriNative(contentUri: Uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI): Uri? { - val contentResolver = reactApplicationContext.contentResolver - val selection = "${MediaStore.MediaColumns.RELATIVE_PATH}=?" - val selectionArgs = arrayOf(bitmapCoverDir) - val cursor = contentResolver.query(contentUri, null, selection,selectionArgs,null) - if (cursor != null && cursor.count > 0) { - while (cursor.moveToNext()) { - val filename = cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME)) - if (filename == bitmapCoverFileName) { - val id = cursor.getLong(cursor.getColumnIndex(MediaStore.MediaColumns._ID)) - cursor.close() - return ContentUris.withAppendedId(contentUri, id) - } - } - cursor.close() - } - return null - } - - @ReactMethod fun getAPMCacheUri(callback: Promise) { - callback.resolve(getAPMCacheUriNative().toString()) - } -} diff --git a/android/app/src/main/java/com/noxplay/noxplayer/NoxAndroidAutoPackage.kt b/android/app/src/main/java/com/noxplay/noxplayer/NoxAndroidAutoPackage.kt deleted file mode 100644 index 9f0ffca0..00000000 --- a/android/app/src/main/java/com/noxplay/noxplayer/NoxAndroidAutoPackage.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.noxplay.noxplayer - -import android.view.View -import com.facebook.react.ReactPackage -import com.facebook.react.bridge.NativeModule -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.uimanager.ReactShadowNode -import com.facebook.react.uimanager.ViewManager - -class NoxPackage : ReactPackage { - - override fun createViewManagers( - reactContext: ReactApplicationContext - ): MutableList>> = mutableListOf() - - override fun createNativeModules( - reactContext: ReactApplicationContext - ): MutableList = listOf(NoxAndroidAutoModule(reactContext)).toMutableList() -} diff --git a/android/app/src/main/java/com/noxplay/noxplayer/NoxModule.kt b/android/app/src/main/java/com/noxplay/noxplayer/NoxModule.kt new file mode 100644 index 00000000..89800d28 --- /dev/null +++ b/android/app/src/main/java/com/noxplay/noxplayer/NoxModule.kt @@ -0,0 +1,135 @@ +package com.noxplay.noxplayer + +import android.app.ActivityManager +import android.app.ApplicationExitInfo +import android.content.ContentUris +import android.content.Context +import android.net.Uri +import android.os.Build +import android.provider.MediaStore +import android.provider.Settings +import androidx.core.content.FileProvider +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.WritableArray +import com.facebook.react.bridge.WritableNativeArray +import timber.log.Timber +import java.io.File + + +class NoxModule(reactContext: ReactApplicationContext) : + ReactContextBaseJavaModule(reactContext) { + override fun getName() = "NoxModule" + + private fun listMediaDirNative(relativeDir: String, subdir: Boolean, selection: String? = null): WritableArray { + val results: WritableArray = WritableNativeArray() + try { + val query = reactApplicationContext.contentResolver.query( + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, + arrayOf( + MediaStore.Audio.Media._ID, + MediaStore.Audio.Media.RELATIVE_PATH, + MediaStore.Audio.Media.DISPLAY_NAME, + MediaStore.Audio.Media.DATA, + MediaStore.Audio.Media.TITLE, + MediaStore.Audio.Media.ALBUM, + MediaStore.Audio.Media.ARTIST, + MediaStore.Audio.Media.BITRATE, + MediaStore.Audio.Media.DURATION, + ), selection,null, null) + query?.use { cursor -> + val pathColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.RELATIVE_PATH) + while (cursor.moveToNext()) { + val mediaPath = cursor.getString(pathColumn) + if (mediaPath == relativeDir || (subdir && mediaPath.startsWith(relativeDir))) { + val mediaItem = Arguments.createMap() + mediaItem.putString("URI", + "content:/" + ContentUris.appendId( + Uri.Builder().path(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.path), + cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID))).build().toString()) + mediaItem.putString("relativePath",mediaPath) + mediaItem.putString("fileName", + cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME)) + ) + mediaItem.putString("realPath", + cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA)) + ) + mediaItem.putString("title", + cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE)) + ) + mediaItem.putString("album", + cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM)) + ) + mediaItem.putString("artist", + cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST)) + ) + mediaItem.putInt("duration", + cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)) + ) + mediaItem.putInt("bitrate", + cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.BITRATE)) + ) + results.pushMap(mediaItem) + } + } + } + } catch (e: Exception) { + Timber.tag("NoxFileUtil").e(e.toString()) + } + return results + } + + @ReactMethod fun getUri(uri: String, callback: Promise) { + callback.resolve(FileProvider.getUriForFile(reactApplicationContext, + "${BuildConfig.APPLICATION_ID}.provider", File(uri)).toString()) + } + + @ReactMethod fun listMediaDir(relativeDir: String, subdir: Boolean, callback: Promise) { + callback.resolve(listMediaDirNative(relativeDir, subdir)) + } + + @ReactMethod fun listMediaFileByFName(filename: String, relDir: String, callback: Promise) { + callback.resolve(listMediaDirNative(relDir, true, + "${MediaStore.Audio.Media.DISPLAY_NAME} IN ('$filename')")) + } + + @ReactMethod fun listMediaFileByID(id: String, callback: Promise) { + callback.resolve(listMediaDirNative("", true, + "${MediaStore.Audio.Media._ID} = $id")) + } + @ReactMethod fun getLastExitReason(callback: Promise) { + try { + val activity = reactApplicationContext.currentActivity + val am = activity?.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val reason = am.getHistoricalProcessExitReasons( + "com.noxplay.noxplayer",0,0 + )[0].reason + callback.resolve(reason in intArrayOf( + ApplicationExitInfo.REASON_USER_REQUESTED, + ApplicationExitInfo.REASON_USER_STOPPED, + ApplicationExitInfo.REASON_EXIT_SELF, + ApplicationExitInfo.REASON_PERMISSION_CHANGE, + ApplicationExitInfo.REASON_PACKAGE_UPDATED, + ApplicationExitInfo.REASON_EXCESSIVE_RESOURCE_USAGE, + ApplicationExitInfo.REASON_LOW_MEMORY, + ApplicationExitInfo.REASON_SIGNALED, + )) + } else { + callback.resolve(true) + } + } catch (e: Exception) { + callback.resolve(true) + } + } + + @ReactMethod fun isGestureNavigationMode(callback: Promise) { + val context = reactApplicationContext + callback.resolve( + Settings.Secure.getInt(context.contentResolver, "navigation_mode", 0) == 2 + ) + } +} diff --git a/android/app/src/main/java/com/noxplay/noxplayer/NoxPackage.kt b/android/app/src/main/java/com/noxplay/noxplayer/NoxPackage.kt new file mode 100644 index 00000000..37bbc963 --- /dev/null +++ b/android/app/src/main/java/com/noxplay/noxplayer/NoxPackage.kt @@ -0,0 +1,22 @@ +package com.noxplay.noxplayer + +import android.view.View +import com.facebook.react.ReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.uimanager.ReactShadowNode +import com.facebook.react.uimanager.ViewManager + +class NoxPackage : ReactPackage { + + override fun createViewManagers( + reactContext: ReactApplicationContext + ): MutableList>> = mutableListOf() + + override fun createNativeModules( + reactContext: ReactApplicationContext + ): MutableList = listOf( + NoxModule(reactContext), + APMWidgetModule(reactContext) + ).toMutableList() +} diff --git a/android/app/src/main/res/drawable-nodpi/example_appwidget_preview.png b/android/app/src/main/res/drawable-nodpi/example_appwidget_preview.png new file mode 100644 index 00000000..894b069a Binary files /dev/null and b/android/app/src/main/res/drawable-nodpi/example_appwidget_preview.png differ diff --git a/android/app/src/main/res/drawable-v21/app_widget_background.xml b/android/app/src/main/res/drawable-v21/app_widget_background.xml new file mode 100644 index 00000000..0e2e16c2 --- /dev/null +++ b/android/app/src/main/res/drawable-v21/app_widget_background.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/android/app/src/main/res/drawable-v21/app_widget_inner_view_background.xml b/android/app/src/main/res/drawable-v21/app_widget_inner_view_background.xml new file mode 100644 index 00000000..0a01afc0 --- /dev/null +++ b/android/app/src/main/res/drawable-v21/app_widget_inner_view_background.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/android/app/src/main/res/layout/a_p_m_widget.xml b/android/app/src/main/res/layout/a_p_m_widget.xml new file mode 100644 index 00000000..97bce3be --- /dev/null +++ b/android/app/src/main/res/layout/a_p_m_widget.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + +