wangzhibo
2024-11-29 e381baaeebc5b34b8c21f0f6d509b37e195af183
 v2
13个文件已修改
3个文件已删除
17个文件已添加
4 文件已重命名
2022 ■■■■■ 已修改文件
.idea/deploymentTargetSelector.xml 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.idea/other.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/build.gradle.kts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/debug/AutoUploadVideos-20241029.apk 补丁 | 查看 | 原始文档 | blame | 历史
app/debug/app-debug.apk 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/AndroidManifest.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shsening/autouploadvideos/AppConfig.kt 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shsening/autouploadvideos/CookieHelper.kt 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shsening/autouploadvideos/HelpFragment.kt 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shsening/autouploadvideos/MainActivity.kt 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shsening/autouploadvideos/MyFragment.kt 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shsening/autouploadvideos/SessionProfileActivity.kt 232 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shsening/autouploadvideos/SessionProfileListFragment.kt 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shsening/autouploadvideos/SessionProfileManager.kt 199 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shsening/autouploadvideos/SessionProfileWebView.kt 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shsening/autouploadvideos/UploadFragment.kt 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shsening/autouploadvideos/UploadListFragment.kt 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shsening/autouploadvideos/UploadSessionActivity.kt 266 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shsening/autouploadvideos/WebviewUtils.kt 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shsening/autouploadvideos/action/ActionDouyin.kt 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shsening/autouploadvideos/action/ActionKuaishou.kt 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shsening/autouploadvideos/action/ActionShipinhao.kt 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shsening/autouploadvideos/action/PlatformActionInterface.kt 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shsening/autouploadvideos/utils/BroadcastReceiver.kt 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shsening/autouploadvideos/utils/DownloadHelper.kt 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shsening/autouploadvideos/utils/Utils.kt 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/activity_main.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/activity_session_profile.xml 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/fragment_help.xml 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/fragment_my.xml 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/fragment_session_profile_list.xml 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/list_item_with_checkbox.xml 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/list_item_with_delete.xml 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/upload_list.xml 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/upload_session.xml 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/values/strings.xml 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
gradle/libs.versions.toml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.idea/deploymentTargetSelector.xml
@@ -4,6 +4,17 @@
    <selectionStates>
      <SelectionState runConfigName="app">
        <option name="selectionMode" value="DROPDOWN" />
        <DropdownSelection timestamp="2024-11-27T11:37:15.668942400Z">
          <Target type="DEFAULT_BOOT">
            <handle>
              <DeviceId pluginId="Default" identifier="serial=127.0.0.1:5555;connection=03e18c1a" />
            </handle>
          </Target>
        </DropdownSelection>
        <DialogSelection />
      </SelectionState>
      <SelectionState runConfigName="SessionProfileActivity">
        <option name="selectionMode" value="DROPDOWN" />
      </SelectionState>
    </selectionStates>
  </component>
.idea/other.xml
@@ -262,7 +262,7 @@
          <option name="codename" value="q6q" />
          <option name="id" value="q6q" />
          <option name="manufacturer" value="Samsung" />
          <option name="name" value="SM-F956B" />
          <option name="name" value="Galaxy Z Fold6" />
          <option name="screenDensity" value="420" />
          <option name="screenX" value="1856" />
          <option name="screenY" value="2160" />
app/build.gradle.kts
@@ -60,6 +60,7 @@
    implementation(libs.androidx.ui.tooling.preview)
    implementation(libs.androidx.material3)
    implementation(libs.material)
    implementation(libs.firebase.storage.ktx)
    testImplementation(libs.junit)
    androidTestImplementation(libs.androidx.junit)
    androidTestImplementation(libs.androidx.espresso.core)
@@ -70,6 +71,5 @@
    implementation(libs.okhttp)
    implementation(libs.appcompat)
    implementation(libs.constraintlayout)
}
app/debug/AutoUploadVideos-20241029.apk
Binary files differ
app/debug/app-debug.apk
Binary files differ
app/src/main/AndroidManifest.xml
@@ -42,6 +42,13 @@
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".SessionProfileActivity"
            android:windowSoftInputMode="adjustResize" />
        <activity android:name=".UploadSessionActivity"
            android:windowSoftInputMode="adjustResize" />
    </application>
</manifest>
app/src/main/java/com/shsening/autouploadvideos/AppConfig.kt
@@ -1,6 +1,11 @@
package com.shsening.autouploadvideos
data class PlatformItem(
    val id: Int,
    val name: String,
    val enabled: Boolean,
    var url: String
)
class AppConfig {
    companion object {
        const val APP_VERSION_NAME = "1.1.0"
@@ -44,5 +49,15 @@
        const val URL_UPLOAD_SERVER_YOUTUBE = "https://creator.douyin.com/creator-micro/content/publish?enter_from=publish_page"
        const val URL_UPLOAD_SERVER_TIKTOK = "https://creator.douyin.com/creator-micro/content/publish?enter_from=publish_page"
        val SESSION_PROFILE_PALTFORM_LISTS = listOf(
            PlatformItem(1,"抖音",true,"https://creator.douyin.com/creator-micro/content/publish?enter_from=publish_page"),
            PlatformItem(2,"快手",true,"https://cp.kuaishou.com/article/publish/video"),
            PlatformItem(3,"视频号",true,"https://channels.weixin.qq.com/platform/post/create"),
            PlatformItem(4,"小红书",false,"https://creator.douyin.com/creator-micro/content/publish?enter_from=publish_page"),
            PlatformItem(5,"YouTube", false,"https://creator.douyin.com/creator-micro/content/publish?enter_from=publish_page"),
            PlatformItem(6,"TikTok", false,"https://creator.douyin.com/creator-micro/content/publish?enter_from=publish_page")
        )
        val SESSION_PROFILE_TYPE: Map<Int, String> = mapOf(0 to "美女跳舞", 1 to "美女穿搭",2 to "AI美景", 3 to "搞笑动物")
    }
}
app/src/main/java/com/shsening/autouploadvideos/CookieHelper.kt
New file
@@ -0,0 +1,62 @@
import android.content.Context
import android.webkit.CookieManager
import com.shsening.autouploadvideos.AppConfig
import org.json.JSONObject
object CookieHelper {
    private const val PREF_NAME = "CookieHelper"
    fun saveSessionProfileUrlCookies(context: Context, sessionProfileId: String, url: String, cookies: String) {
        val sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
        val sessionCookiesMap = sharedPreferences.getString(sessionProfileId, "{}")
        val sessionCookies = JSONObject(sessionCookiesMap!!)
        sessionCookies.put(url, cookies)
        sharedPreferences.edit().putString(sessionProfileId, sessionCookies.toString()).apply()
    }
    fun getAllSessionProfileCookies(context: Context, sessionProfileId: String): Map<String, String> {
        val sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
        val sessionCookiesMap = sharedPreferences.getString(sessionProfileId, "{}")
        val sessionCookies = JSONObject(sessionCookiesMap!!)
        return sessionCookies.keys().asSequence().associateWith { sessionCookies.getString(it) }
    }
    fun removeCookiesForUrl(url: String) {
        val cookieManager = CookieManager.getInstance()
        val cookies = cookieManager.getCookie(url) // 获取特定 URL 的所有 Cookie
        if (cookies != null) {
            val cookieArray = cookies.split(";") // 按分号分隔多个 Cookie
            for (cookie in cookieArray) {
                val cookieParts = cookie.split("=")
                if (cookieParts.size > 0) {
                    val cookieName = cookieParts[0].trim()
                    // 设置一个过期的 Cookie 覆盖原来的
                    cookieManager.setCookie(url, "$cookieName=; Expires=Thu, 01 Jan 1970 00:00:00 GMT")
                }
            }
            // 确保将更改同步到存储中
            cookieManager.flush()
        }
    }
    fun removeCookieForAllPlatform(){
        AppConfig.SESSION_PROFILE_PALTFORM_LISTS.forEach { item ->
            removeCookiesForUrl(item.url)
        }
    }
    fun saveSessionCookies(context: Context,sessionProfileId:String) {
        var cookieManager = CookieManager.getInstance()
        AppConfig.SESSION_PROFILE_PALTFORM_LISTS.forEach { item ->
            val cookies = cookieManager.getCookie(item.url)
            if (cookies != null) {
                saveSessionProfileUrlCookies(
                    context,
                    sessionProfileId,
                    item.url,
                    cookies
                )
            }
        }
    }
}
app/src/main/java/com/shsening/autouploadvideos/HelpFragment.kt
@@ -1,12 +1,17 @@
package com.shsening.autouploadvideos
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.WebView
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.fragment.app.Fragment
class HelpFragment : Fragment() {
@@ -17,8 +22,26 @@
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_help, container, false)
        val textView = view.findViewById<TextView>(R.id.androidIdText);
        textView.text = "Android ID: ${AppConfig.CLIENT_ANDROID_ID }"
        textView.setOnClickListener {
            val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
            val clip = ClipData.newPlainText("label", textView.text)
            clipboard.setPrimaryClip(clip)
            Toast.makeText(requireContext(), AppConfig.MSG_MY_TAB_COPY_TO_CLIPBOARD, Toast.LENGTH_SHORT).show()
        }
        // 加载图片
        val imageView = view.findViewById<ImageView>(R.id.imageView)
        imageView.setImageResource(R.drawable.qrcode) // 请替换为你的图片
        // 设置简介
        view.findViewById<TextView>(R.id.descriptionText).text = AppConfig.MSG_MY_TAB_ADD_WECHAT
        webView = view.findViewById(R.id.webView)
        webView.loadUrl("file:///android_asset/help/help.html")
        return view
    }
}
app/src/main/java/com/shsening/autouploadvideos/MainActivity.kt
@@ -2,41 +2,22 @@
import android.Manifest
import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.View
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity
import android.graphics.Color
import android.net.Uri
import android.provider.Settings
import android.util.Log
import android.webkit.WebView
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.FileProvider
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import com.shsening.autouploadvideos.utils.Utils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.Call
import okhttp3.Callback
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
import java.io.IOException
import kotlinx.coroutines.*
import okhttp3.internal.wait
import java.io.FileOutputStream
import java.util.concurrent.CountDownLatch
class MainActivity : AppCompatActivity() {
@@ -66,20 +47,26 @@
        fragmentManager = supportFragmentManager
        // 默认显示第一个 Fragment
        /*
        setCurrentFragment(UploadFragment())
        findViewById<Button>(R.id.tabUpload).setOnClickListener {
            setCurrentFragment(UploadFragment())
            highlightTab(it)
        }
         */
        setCurrentFragment(UploadListFragment())
        findViewById<Button>(R.id.tabUpload).setOnClickListener {
            setCurrentFragment(UploadListFragment())
            highlightTab(it)
        }
        findViewById<Button>(R.id.tabHelp).setOnClickListener {
            setCurrentFragment(HelpFragment())
        findViewById<Button>(R.id.tabSetting).setOnClickListener {
            setCurrentFragment(SessionProfileListFragment())
            highlightTab(it)
        }
        findViewById<Button>(R.id.tabMy).setOnClickListener {
            setCurrentFragment(MyFragment())
            setCurrentFragment(HelpFragment())
            highlightTab(it)
        }
        CoroutineScope(Dispatchers.IO).launch {
@@ -106,7 +93,7 @@
    private fun highlightTab(selectedButton: View) {
        val tabButtons = listOf(
            findViewById<Button>(R.id.tabUpload),
            findViewById<Button>(R.id.tabHelp),
            findViewById<Button>(R.id.tabSetting),
            findViewById<Button>(R.id.tabMy)
        )
app/src/main/java/com/shsening/autouploadvideos/MyFragment.kt
File was deleted
app/src/main/java/com/shsening/autouploadvideos/SessionProfileActivity.kt
New file
@@ -0,0 +1,232 @@
package com.shsening.autouploadvideos
import CookieHelper
import CookieHelper.saveSessionProfileUrlCookies
import Profile
import SessionProfile
import SessionProfileManager
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.View
import android.webkit.CookieManager
import android.webkit.WebChromeClient
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.Spinner
import android.widget.Switch
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import java.util.UUID
class SessionProfileActivity : AppCompatActivity() {
    private lateinit var sessionProfileManager: SessionProfileManager
    private lateinit var sessionProfile: SessionProfile
    private val appConfigItems = AppConfig.SESSION_PROFILE_PALTFORM_LISTS // 获取平台信息列表
    private lateinit var editName: EditText
    private lateinit var spinnerType: Spinner
    private lateinit var switchContainer: LinearLayout
    private lateinit var sessionProfileWebView:SessionProfileWebView
    private var isNewSessionProfile:Boolean = false;
    private lateinit var progressBar: ProgressBar
    private lateinit var activityReference: SessionProfileActivity
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_session_profile)
        activityReference = this
        // 获取传入的 SessionProfile
        sessionProfile = intent.getSerializableExtra("sessionProfile") as? SessionProfile
            ?: SessionProfile(
                id = "",
                name = "",
                type = 0,
                lastUploadTime = "",
                profiles = mutableListOf()
            )
        if (sessionProfile.id == ""){
            isNewSessionProfile = true
            sessionProfile.id = UUID.randomUUID().toString().replace("-", "")
            findViewById<TextView>(R.id.txt_session_id).text = "new:"+sessionProfile.id
        }else {
            findViewById<TextView>(R.id.txt_session_id).text = sessionProfile.id
        }
        editName = findViewById(R.id.edit_name)
        findViewById<TextView>(R.id.edit_name).text = sessionProfile.name
        spinnerType = findViewById(R.id.spinner_type)
        spinnerType.adapter = ArrayAdapter(
            this,
            android.R.layout.simple_spinner_item,
            AppConfig.SESSION_PROFILE_TYPE.values.toList()
        )
        spinnerType.setSelection(sessionProfile.type)
        switchContainer = findViewById(R.id.switch_container)
        setupSwitches()
        // 初始化 SessionProfileManager
        sessionProfileManager = SessionProfileManager(this)
        progressBar = findViewById(R.id.progressBar)
        sessionProfileWebView = findViewById<SessionProfileWebView>(R.id.sessionProfileWebView)
        sessionProfileWebView.setInitialScale(75) // 初始缩放比例
        sessionProfileWebView.isFocusableInTouchMode = true
        try{
            Log.e("shsening", sessionProfile.id)
            Log.e("shsening", CookieManager.getInstance().getCookie(AppConfig.URL_UPLOAD_SERVER_DOUYIN))
        }catch(e:Exception){
            Log.e("shsening", "there is no cookie founded")
        }
        sessionProfileWebView.loadSessionProfileCookies(sessionProfile.id)
        try{
            Log.e("shsening", CookieManager.getInstance().getCookie(AppConfig.URL_UPLOAD_SERVER_DOUYIN))
        }catch(e:Exception){
            Log.e("shsening", "there is no cookie founded")
        }
        sessionProfileWebView.webViewClient = object : WebViewClient() {
            override fun onPageStarted(
                view: WebView?,
                url: String?,
                favicon: android.graphics.Bitmap?
            ) {
                super.onPageStarted(view, url, favicon)
                progressBar.visibility = View.VISIBLE
            }
            override fun onPageFinished(view: WebView?, url: String?) {
                super.onPageFinished(view, url)
                progressBar.visibility = View.GONE
                url?.let{
                    Log.e("shsening", url)
                    try {
                        val cookies = CookieManager.getInstance().getCookie(url)
                        if (cookies != null) {
                            CookieHelper.saveSessionProfileUrlCookies(
                                activityReference,
                                sessionProfile.id,
                                url,
                                cookies
                            )
                        } else {
                            Log.w("shsening", "No cookies found for URL: $url")
                        }
                    } catch (e: java.lang.Exception) {
                        Log.e("CookieHelper", "Failed to get cookies: ", e)
                    }
                }
            }
        }
        sessionProfileWebView.webChromeClient = object : WebChromeClient() {
            override fun onShowCustomView(view: View?, callback: CustomViewCallback?) {
                super.onShowCustomView(view, callback)
                view?.requestFocus()
            }
        }
        // 保存按钮的监听
        findViewById<TextView>(R.id.btn_save).setOnClickListener {
            saveSessionProfile()
        }
        findViewById<TextView>(R.id.btn_back).setOnClickListener {
            finish()
        }
    }
    private fun setupSwitches() {
        switchContainer.removeAllViews()
        // 创建一个水平的 LinearLayout
        var horizontalLayout: LinearLayout? = null
        // 遍历从 AppConfig 获取的所有平台项目
        for ((index, platform) in appConfigItems.withIndex()) {
            if (index % 2 == 0) {
                // 每两项创建一个新的水平布局
                horizontalLayout = LinearLayout(this).apply {
                    orientation = LinearLayout.HORIZONTAL
                    layoutParams = LinearLayout.LayoutParams(
                        LinearLayout.LayoutParams.MATCH_PARENT,
                        LinearLayout.LayoutParams.WRAP_CONTENT
                    )
                }
            }
            val switchView:Switch = Switch(this).apply {
                text = platform.name
                val existingProfile = sessionProfile.profiles.find { it.name == platform.name }
                isChecked = existingProfile?.status ?: false
                isEnabled = platform.enabled
                // 开关监听器
                setOnCheckedChangeListener { _, isChecked ->
                    if (existingProfile != null) {
                        existingProfile.status = isChecked
                    } else {
                        sessionProfile.addOrUpdateProfile(Profile(name = platform.name, status = isChecked))
                    }
                    if (isChecked) {
                        //sessionProfileWebView.loadSessionProfileCookies(sessionProfile.id)
                        sessionProfileWebView.loadUrl(platform.url)
                    }
                }
                layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f).apply {
                    marginStart = 8
                    marginEnd = 8
                }
            }
            // 添加 Switch 到水平布局
            horizontalLayout?.addView(switchView)
            // 如果是偶数索引或者最后一个项目,添加水平布局到主容器
            if (index % 2 == 1 || index == appConfigItems.size - 1) {
                switchContainer.addView(horizontalLayout)
            }
        }
    }
    private fun saveSessionProfile() {
        try {
            CookieHelper.saveSessionCookies(this,sessionProfile.id)
            val name = editName.text.toString().trim() // 获取输入的名称
            if (name.isEmpty()) {
                Toast.makeText(this, "Name cannot be empty!", Toast.LENGTH_SHORT).show()
                return
            }
            if (isNewSessionProfile and sessionProfileManager.checkProfileNameExist(name)){
                Toast.makeText(this, "Name already exist !", Toast.LENGTH_SHORT).show()
                return // 不保存
            }
            sessionProfile.name = name
            sessionProfile.type = spinnerType.selectedItemPosition
            sessionProfileManager.addOrUpdateSessionProfile(sessionProfile)
            sessionProfileManager.saveToFile()
            Toast.makeText(this, "Session profile saved", Toast.LENGTH_SHORT).show()
            setResult(Activity.RESULT_OK, Intent())
            finish()
        }catch (e:Exception){
            e.printStackTrace()
        }
    }
}
app/src/main/java/com/shsening/autouploadvideos/SessionProfileListFragment.kt
New file
@@ -0,0 +1,130 @@
package com.shsening.autouploadvideos
import SessionProfile
import SessionProfileManager
import android.app.Activity
import android.app.AlertDialog
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.ListView
import androidx.fragment.app.Fragment
import android.widget.TextView
import com.shsening.autouploadvideos.utils.Utils
import java.io.Serializable
class SessionProfileListFragment : Fragment() {
    private lateinit var listView: ListView
    private lateinit var btnNew: View
    private val REQUEST_CODE = 100
    private val sessionProfiles = mutableListOf<SessionProfile>() // Replace with your real data source
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        val view = inflater.inflate(R.layout.fragment_session_profile_list, container, false)
        listView = view.findViewById(R.id.list_view)
        btnNew = view.findViewById(R.id.btn_new)
        setupListView()
        setupNewButton()
        return view
    }
    private fun setupListView() {
        // 清空适配器
        listView.adapter = null
        // 从 SessionProfileManager 获取所有的 SessionProfile
        val sessionProfiles = SessionProfileManager(requireContext()).getAllProfiles()
        // 自定义适配器实现删除按钮功能
        val adapter = object : ArrayAdapter<SessionProfile>(
            requireContext(),
            android.R.layout.simple_list_item_2,
            sessionProfiles
        ) {
            override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
                val inflater = LayoutInflater.from(context)
                val view = convertView ?: inflater.inflate(R.layout.list_item_with_delete, parent, false)
                // 获取当前的 SessionProfile
                val sessionProfile = getItem(position)
                // 填充数据
                val text1 = view.findViewById<TextView>(android.R.id.text1)
                val textType = Utils.getSessionProfileValueByPosition(sessionProfile?.type)
                text1.text = "${sessionProfile?.name} (${textType})"
                val text2 = view.findViewById<TextView>(android.R.id.text2)
                text2.text = sessionProfile?.lastUploadTime
                // 设置删除按钮
                val deleteButton = view.findViewById<Button>(R.id.deleteButton)
                deleteButton.setOnClickListener {
                    // 弹出确认对话框
                    AlertDialog.Builder(context)
                        .setTitle("删除确认")
                        .setMessage("确定要删除 ${sessionProfile?.name} 吗?")
                        .setPositiveButton("确认") { _, _ ->
                            // 从管理器中删除
                            sessionProfile?.let {
                                SessionProfileManager(requireContext()).removeProfile(it.id)
                            }
                            // 刷新列表
                            setupListView()
                        }
                        .setNegativeButton("取消", null)
                        .show()
                }
                // 设置行点击事件
                view.setOnClickListener {
                    val intent = Intent(requireContext(), SessionProfileActivity::class.java)
                    intent.putExtra("sessionProfile", sessionProfile as Serializable) // 传递选中的 Profile 对象
                    startActivityForResult(intent, REQUEST_CODE)
                }
                return view
            }
        }
        // 设置适配器到 ListView
        listView.adapter = adapter
        // 设置点击事件
        listView.onItemClickListener =
            AdapterView.OnItemClickListener { _, _, position, _ ->
                val selectedProfile = sessionProfiles[position]
                val intent = Intent(requireContext(), SessionProfileActivity::class.java)
                intent.putExtra("sessionProfile", selectedProfile as Serializable) // 传递选中的 Profile 对象
                startActivityForResult(intent, REQUEST_CODE)
            }
    }
    // 接收到 Activity 返回结果时刷新 Fragment
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK) {
            // 刷新 Fragment 内容
            setupListView()
        }
    }
    private fun setupNewButton() {
        btnNew.setOnClickListener {
            val intent = Intent(requireContext(), SessionProfileActivity::class.java)
            startActivityForResult(intent, REQUEST_CODE)
        }
    }
}
app/src/main/java/com/shsening/autouploadvideos/SessionProfileManager.kt
New file
@@ -0,0 +1,199 @@
import android.content.Context
import android.os.Parcel
import com.shsening.autouploadvideos.AppConfig
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
import java.io.Serializable
import android.os.Parcelable
import com.shsening.autouploadvideos.utils.Utils
data class SessionProfile(
    var id: String,
    var name: String,
    var type: Int,
    var lastUploadTime: String,
    val profiles: MutableList<Profile> = mutableListOf()
) : Parcelable, Serializable { // 逗号分隔接口
    // Parcelable 的实现
    override fun writeToParcel(parcel: Parcel, flags: Int) {
        parcel.writeString(id)
        parcel.writeString(name)
        parcel.writeInt(type)
        parcel.writeString(lastUploadTime)
        parcel.writeTypedList(profiles) // 对于列表,使用 writeTypedList
    }
    override fun describeContents(): Int {
        return 0
    }
    // Parcelable 的 Companion Object
    companion object CREATOR : Parcelable.Creator<SessionProfile> {
        override fun createFromParcel(parcel: Parcel): SessionProfile {
            val id = parcel.readString().orEmpty()
            val name = parcel.readString().orEmpty()
            val type = parcel.readInt()
            val lastUploadTime = parcel.readString().orEmpty()
            val profiles = mutableListOf<Profile>().apply {
                parcel.readTypedList(this, Profile.CREATOR)
            }
            return SessionProfile(id, name, type, lastUploadTime, profiles)
        }
        override fun newArray(size: Int): Array<SessionProfile?> {
            return arrayOfNulls(size)
        }
    }
    // 添加或更新 Profile 的方法
    fun addOrUpdateProfile(profile: Profile) {
        val existingProfile = profiles.find { it.name == profile.name }
        if (existingProfile != null) {
            existingProfile.status = profile.status
        } else {
            profiles.add(profile)
        }
    }
    fun getValidProfileNames(): List<String> {
        return profiles.filter { it.status }.map { it.name }
    }
}
data class Profile(
    var name: String,
    var status: Boolean
) : Serializable,Parcelable {
    override fun writeToParcel(parcel: Parcel, flags: Int) {
        parcel.writeString(name)
        parcel.writeByte(if (status) 1 else 0)
    }
    override fun describeContents(): Int {
        return 0
    }
    companion object CREATOR : Parcelable.Creator<Profile> {
        override fun createFromParcel(parcel: Parcel): Profile {
            val name = parcel.readString().orEmpty()
            val status = parcel.readByte() != 0.toByte()
            return Profile(name, status)
        }
        override fun newArray(size: Int): Array<Profile?> {
            return arrayOfNulls(size)
        }
    }
}
/**
 * val profileManager = SessionProfileManager(context)
 * profileManager.addProfile(1, "douyin", true)
 * val profiles = profileManager.getProfile(1)
 * profileManager.removeProfile(1)
 * val allProfiles = profileManager.getAllProfiles()
 */
class SessionProfileManager(private val context: Context) {
    companion object {
        private const val FILE_NAME = "session_profiles.json"
    }
    private val sessionProfiles = mutableListOf<SessionProfile>()
    init {
        ensureFileExists() // 确保文件存在
        loadProfiles()
    }
    // 确保文件存在
    private fun ensureFileExists() {
        val file = File(context.filesDir, FILE_NAME)
        if (!file.exists()) {
            file.writeText("[]") // 创建空 JSON 数组作为初始内容
        }
    }
    // 加载文件内容
    private fun loadProfiles() {
        val file = File(context.filesDir, FILE_NAME)
        val jsonString = file.readText()
        val jsonArray = JSONArray(jsonString)
        sessionProfiles.clear()
        for (i in 0 until jsonArray.length()) {
            val jsonObj = jsonArray.getJSONObject(i)
            sessionProfiles.add(jsonToSessionProfile(jsonObj))
        }
    }
    // 保存到文件
    private fun saveProfiles() {
        val jsonArray = JSONArray()
        sessionProfiles.forEach { profile ->
            jsonArray.put(sessionProfileToJson(profile))
        }
        val file = File(context.filesDir, FILE_NAME)
        file.writeText(jsonArray.toString())
    }
    fun saveToFile() {
        saveProfiles()
    }
    // Convert JSON to SessionProfile
    private fun jsonToSessionProfile(json: JSONObject): SessionProfile {
        val id = json.getString("id")
        val name = json.getString("name")
        val type = json.getInt("type")
        val lastUploadTime = json.getString("lastUploadTime")
        val profilesJsonArray = json.getJSONArray("profiles")
        val profiles = mutableListOf<Profile>()
        for (i in 0 until profilesJsonArray.length()) {
            val profileJson = profilesJsonArray.getJSONObject(i)
            profiles.add(Profile(profileJson.getString("name"), profileJson.getBoolean("status")))
        }
        return SessionProfile(id, name, type, lastUploadTime, profiles)
    }
    // Convert SessionProfile to JSON
    private fun sessionProfileToJson(profile: SessionProfile): JSONObject {
        val json = JSONObject()
        json.put("id", profile.id)
        json.put("name", profile.name)
        json.put("type", profile.type)
        json.put("lastUploadTime", profile.lastUploadTime)
        val profilesJsonArray = JSONArray()
        profile.profiles.forEach {
            val profileJson = JSONObject()
            profileJson.put("name", it.name)
            profileJson.put("status", it.status)
            profilesJsonArray.put(profileJson)
        }
        json.put("profiles", profilesJsonArray)
        return json
    }
    // Get all session profiles
    fun getAllProfiles(): List<SessionProfile> {
        if (sessionProfiles.isEmpty()) {
            loadProfiles()
        }
        return sessionProfiles.toList() // 将内部的 profiles 返回为不可变列表
    }
    // Add a new session profile
    fun checkProfileNameExist(name: String): Boolean {
        return sessionProfiles.any { it.name == name }
    }
    // Remove a session profile
    fun removeProfile(id: String) {
        sessionProfiles.removeAll { it.id == id }
        saveProfiles()
    }
    // 3. 添加或更新 SessionProfile
    fun addOrUpdateSessionProfile(sessionProfile: SessionProfile) {
        val existingProfile = sessionProfiles.find { it.id == sessionProfile.id }
        if (existingProfile != null) {
            val index = sessionProfiles.indexOf(existingProfile)
            sessionProfiles[index] = sessionProfile
        } else {
            // 添加新的 Profile
            sessionProfiles.add(sessionProfile)
        }
    }
}
app/src/main/java/com/shsening/autouploadvideos/SessionProfileWebView.kt
New file
@@ -0,0 +1,118 @@
package com.shsening.autouploadvideos
import CookieHelper
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.text.TextUtils
import android.util.AttributeSet
import android.util.Log
import android.webkit.CookieManager
import android.webkit.CookieSyncManager
import android.webkit.WebSettings
import android.webkit.WebView
import android.widget.Toast
class SessionProfileWebView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : WebView(context, attrs, defStyle) {
    init {
        settings.javaScriptEnabled = true
        settings.userAgentString = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
        settings.javaScriptEnabled = true
        settings.domStorageEnabled = true
        settings.databaseEnabled = true
        settings.setSupportZoom(true)
        settings.builtInZoomControls = true
        settings.displayZoomControls = false
        settings.allowFileAccess = true
        settings.textZoom = 50 // Set zoom level to 30%
        settings.useWideViewPort = true
        settings.loadWithOverviewMode = true
        settings.layoutAlgorithm = WebSettings.LayoutAlgorithm.NORMAL
        CookieManager.getInstance().setAcceptCookie(true) // 接受 Cookie
        CookieManager.getInstance().setAcceptThirdPartyCookies(this, true) // 接受第三方 Cookie
        settings.setCacheMode(WebSettings.LOAD_NO_CACHE)
    }
    // 加载 SessionProfile 的所有 URL-Cookie 映射到 WebView
    fun loadSessionProfileCookies(sessionProfileId: String) {
        //CookieHelper.removeCookieForAllPlatform()
        CookieManager.getInstance().removeAllCookies(null)
        CookieManager.getInstance().flush()
        val sessionCookies = CookieHelper.getAllSessionProfileCookies(context, sessionProfileId)
        for ((url, cookies) in sessionCookies) {
            splitCookieStringAndSetCookie(url,cookies)
        }
    }
    private fun splitCookieStringAndSetCookie(url:String,cookies:String){
        val cookie_part = cookies.split(";".toRegex()).dropLastWhile { it.isEmpty() }
            .toTypedArray()
        val cookieManager = CookieManager.getInstance()
        if (cookie_part!=null) {
            for (cook in cookie_part) {
                //一定要一个一个set, 如果是拼接的话,设置不成功。
                //cookieManager.setCookie(url, cook.trim{ it <= ' ' })
                cookieManager.setCookie(url, cook.trim())
            }
        }
        cookieManager.flush()
    }
    // 访问某个 URL 时,保存当前 SessionProfile 的 Cookie
    fun saveCurrentSessionProfileUrlCookies(sessionProfileId:String, url: String) {
        val cookieManager = CookieManager.getInstance()
        val cookies = cookieManager.getCookie(url)
        if (cookies != null) {
            CookieHelper.saveSessionProfileUrlCookies(context, sessionProfileId, url, cookies)
        }
    }
    override fun loadUrl(url: String) {
        try{
            val headers = mapOf(
                "pragma" to "no-cache",
                "priority" to "u=0, i",
                "sec-ch-ua" to "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"",
                "sec-ch-ua-mobile" to "?0",
                "sec-ch-ua-platform" to "\"Windows\"",
                "sec-fetch-dest" to "document",
                "sec-fetch-mode" to "navigate",
                "sec-fetch-site" to "none",
                "sec-fetch-user" to "?1",
                "upgrade-insecure-requests" to "1"
                )
            super.loadUrl(url,headers)
        }catch (e:Exception){
            e.printStackTrace()
        }
    }
    fun loadUrlNoCache(url: String) {
        try{
            val headers = mapOf(
                "pragma" to "no-cache",
                "priority" to "u=0, i",
                "sec-ch-ua" to "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"",
                "sec-ch-ua-mobile" to "?0",
                "sec-ch-ua-platform" to "\"Windows\"",
                "sec-fetch-dest" to "document",
                "sec-fetch-mode" to "navigate",
                "sec-fetch-site" to "none",
                "sec-fetch-user" to "?1",
                "upgrade-insecure-requests" to "1"
            )
            super.clearCache(true)
            super.loadUrl(url,headers)
        }catch (e:Exception){
            e.printStackTrace()
        }
    }
}
app/src/main/java/com/shsening/autouploadvideos/UploadFragment.kt
@@ -19,6 +19,10 @@
import androidx.core.content.FileProvider
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.shsening.autouploadvideos.action.ActionDouyin
import com.shsening.autouploadvideos.action.ActionKuaishou
import com.shsening.autouploadvideos.action.ActionShipinhao
import com.shsening.autouploadvideos.utils.Utils
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import okhttp3.Call
@@ -49,7 +53,7 @@
    var uploadTaskList: MutableList<Utils.VideoUploadTaskInfo> =mutableListOf()
    private lateinit var currentUploadVideoInfo:Utils.VideoUploadTaskInfo
    private lateinit var currentUploadVideoInfo: Utils.VideoUploadTaskInfo
    private lateinit var currentUploadServerUrl:String
    private val threadHandler = Handler(Looper.getMainLooper())
@@ -68,11 +72,8 @@
            override fun onPageFinished(view: WebView?, url: String?) {
                super.onPageFinished(view, url)
                view?.requestFocus()
                injectPageLoadFinishedActions()
            }
        }
@@ -146,6 +147,8 @@
        try {
            /*
            when (currentUploadServerType) {
                AppConfig.UPLOADSERVERTYPE_DOUYIN -> {
                    val actionDouyin =
@@ -177,6 +180,8 @@
                }
            }
             */
        }catch (e:Exception){
            e.printStackTrace()
        }
@@ -309,10 +314,9 @@
                    currentUploadServerIndex++
                }else{
                    lifecycleScope.launch {
                        Utils.sendVideoUploadedMessage(
                            requireContext(),
                            currentUploadVideoInfo.taskId
                        )
                        Utils.sendVideoUploadedMessage(currentUploadVideoInfo.taskId) { result, error ->
                            Toast.makeText(context,error.toString(),Toast.LENGTH_SHORT)
                        }
                    }
                    startUploadTask()
                }
@@ -325,7 +329,7 @@
    }
    private fun uploadVideoUrlAndWait(taskInfo:Utils.VideoUploadTaskInfo){
    private fun uploadVideoUrlAndWait(taskInfo: Utils.VideoUploadTaskInfo){
        try{
            currentUploadVideoInfo = taskInfo
@@ -370,7 +374,7 @@
    private suspend fun executeDownloadAndUploadTask(taskInfo:Utils.VideoUploadTaskInfo){
    private suspend fun executeDownloadAndUploadTask(taskInfo: Utils.VideoUploadTaskInfo){
        Utils.downloadCommonFileToExternalFilesDir(requireContext(),taskInfo.videoUrl,taskInfo.filename+".mp4",false)
        Utils.downloadCommonFileToExternalFilesDir(requireContext(),taskInfo.coverUrl,taskInfo.filename+".png",false)
    }
app/src/main/java/com/shsening/autouploadvideos/UploadListFragment.kt
New file
@@ -0,0 +1,144 @@
package com.shsening.autouploadvideos
import SessionProfile
import SessionProfileManager
import android.app.Activity
import android.app.AlertDialog
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.CheckBox
import android.widget.ListView
import androidx.fragment.app.Fragment
import android.widget.TextView
import com.shsening.autouploadvideos.utils.Utils
class UploadListFragment : Fragment() {
    private lateinit var listView: ListView
    private lateinit var btnAllTask: View
    private val REQUEST_CODE = 100
    private val sessionProfiles = mutableListOf<SessionProfile>() // Replace with your real data source
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        val view = inflater.inflate(R.layout.upload_list, container, false)
        listView = view.findViewById(R.id.upload_list_view)
        btnAllTask = view.findViewById(R.id.btn_all_task)
        setupListView()
        setupAllTaskButton()
        return view
    }
    private fun setupListView() {
        // 清空适配器
        listView.adapter = null
        // 从 SessionProfileManager 获取所有的 SessionProfile
        val sessionProfiles = SessionProfileManager(requireContext()).getAllProfiles()
        // 自定义适配器实现删除按钮功能
        val adapter = object : ArrayAdapter<SessionProfile>(
            requireContext(),
            android.R.layout.simple_list_item_2,
            sessionProfiles
        ) {
            override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
                val inflater = LayoutInflater.from(context)
                val view = convertView ?: inflater.inflate(R.layout.list_item_with_checkbox, parent, false)
                // 获取当前的 SessionProfile
                val sessionProfile = getItem(position)
                // 填充数据
                val text1 = view.findViewById<TextView>(android.R.id.text1)
                val textType = Utils.getSessionProfileValueByPosition(sessionProfile?.type)
                text1.text = "${sessionProfile?.name} (${textType})"
                val text2 = view.findViewById<TextView>(android.R.id.text2)
                text2.text = sessionProfile?.lastUploadTime
                // 设置删除按钮
                val deleteButton = view.findViewById<Button>(R.id.deleteButton)
                deleteButton.setOnClickListener {
                    val intent = Intent(requireContext(), UploadSessionActivity::class.java)
                    val sessionProfiles = arrayListOf(sessionProfile) // 替换为实际的选定数组
                    intent.putParcelableArrayListExtra("sessionProfiles", sessionProfiles)
                    startActivityForResult(intent, REQUEST_CODE)
                }
                return view
            }
        }
        // 设置适配器到 ListView
        listView.adapter = adapter
    }
    fun getSelectedSessionProfiles(): ArrayList<SessionProfile> {
        val selectedProfiles = ArrayList<SessionProfile>()
        // 获取 ListView 的适配器
        val adapter = listView.adapter as? ArrayAdapter<*>
        // 确保适配器正确
        if (adapter != null) {
            // 遍历 ListView 中的所有项
            for (i in 0 until adapter.count) {
                // 获取当前的视图
                val view = listView.getChildAt(i)
                // 获取 CheckBox 组件
                val checkBox = view?.findViewById<CheckBox>(R.id.itemCheckBox)
                // 检查 CheckBox 是否被选中
                if (checkBox?.isChecked == true) {
                    // 获取对应的 SessionProfile 对象
                    val sessionProfile = adapter.getItem(i) as? SessionProfile
                    // 将选中的 SessionProfile 添加到结果列表中
                    sessionProfile?.let {
                        selectedProfiles.add(it)
                    }
                }
            }
        }
        return selectedProfiles
    }
    // 接收到 Activity 返回结果时刷新 Fragment
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK) {
            // 刷新 Fragment 内容
            setupListView()
        }
    }
    private fun setupAllTaskButton() {
        btnAllTask.setOnClickListener {
            var selected = getSelectedSessionProfiles()
            if (selected.isEmpty()){
                    AlertDialog.Builder(requireContext())
                        .setTitle("提示")
                        .setMessage("请至少选择一个项目!")
                        .setPositiveButton("确定", null)
                        .show()
            }else {
                val intent = Intent(requireContext(), UploadSessionActivity::class.java)
                intent.putParcelableArrayListExtra("sessionProfiles", selected)
                startActivityForResult(intent, REQUEST_CODE)
            }
        }
    }
}
app/src/main/java/com/shsening/autouploadvideos/UploadSessionActivity.kt
New file
@@ -0,0 +1,266 @@
package com.shsening.autouploadvideos
import CookieHelper
import Profile
import SessionProfile
import SessionProfileManager
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.View
import android.webkit.CookieManager
import android.webkit.ValueCallback
import android.webkit.WebChromeClient
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.Spinner
import android.widget.Switch
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
import com.shsening.autouploadvideos.AppConfig.Companion.SESSION_PROFILE_PALTFORM_LISTS
import com.shsening.autouploadvideos.action.ActionDouyin
import com.shsening.autouploadvideos.action.ActionKuaishou
import com.shsening.autouploadvideos.action.ActionShipinhao
import com.shsening.autouploadvideos.utils.DownloadHelper
import com.shsening.autouploadvideos.utils.DownloadReceiver
import com.shsening.autouploadvideos.utils.Utils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.Call
import okhttp3.Callback
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.json.JSONArray
import java.io.File
import java.io.IOException
import java.util.LinkedList
import java.util.Queue
import java.util.UUID
class UploadSessionActivity : AppCompatActivity() {
    private lateinit var sessionProfileManager: SessionProfileManager
    lateinit var sessionProfileWebView:SessionProfileWebView
    private lateinit var uploadSessionText :TextView
    private lateinit var downloadHelper:DownloadHelper
    private val uploadTaskQueue: Queue<Utils.VideoUploadTaskInfo> = LinkedList() // 存储上传任务队列
    private lateinit var sessionProfiles: java.util.ArrayList<SessionProfile>
    private var last_uploaded_task_id:String = ""
    private val threadHandler = Handler(Looper.getMainLooper())
    private lateinit var activityReference: UploadSessionActivity
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.upload_session)
        activityReference = this
        // 初始化 SessionProfileManager
        sessionProfileManager = SessionProfileManager(this)
        //downloadReceiver = DownloadReceiver(this,)
        uploadSessionText = findViewById(R.id.uploadSessionText)
        sessionProfileWebView = findViewById(R.id.uploadSessionWebView)
        // 获取传递的 SessionProfile 数组
        sessionProfiles =
            intent.getParcelableArrayListExtra<SessionProfile>("sessionProfiles")
                ?: arrayListOf()
        if (sessionProfiles != null) {
            for (profile in sessionProfiles) {
                processProfile(profile)
            }
        } else {
            Toast.makeText(this, "未接收到任何 Profile 数据", Toast.LENGTH_SHORT).show()
        }
        downloadHelper = DownloadHelper(this)
        //sessionProfileWebView.setInitialScale(75) // 初始缩放比例
        sessionProfileWebView.webViewClient = object : WebViewClient() {
            override fun onPageStarted(
                view: WebView?,
                url: String?,
                favicon: android.graphics.Bitmap?
            ) {
                super.onPageStarted(view, url, favicon)
            }
            override fun onPageFinished(view: WebView?, url: String?) {
                super.onPageFinished(view, url)
                view?.requestFocus() // 确保 WebView 获取焦点
                //inject action
                val currentTask = uploadTaskQueue.peek()
                when (currentTask?.platform?.name) {
                    "抖音" -> {
                        val action =
                            ActionDouyin(activityReference, threadHandler, currentTask)
                        action.checkIfContentLoaded()
                    }
                    "快手" -> {
                        val action =
                            ActionKuaishou(activityReference, threadHandler, currentTask)
                        action.checkIfContentLoaded()
                    }
                    "视频号" -> {
                        val action =
                            ActionShipinhao(activityReference, threadHandler, currentTask)
                        action.checkIfContentLoaded()
                    }
                }
                url?.let{
                    Log.e("shsening", url)
                    try {
                        val cookies = CookieManager.getInstance().getCookie(url)
                        if (cookies != null) {
                            CookieHelper.saveSessionProfileUrlCookies(
                                activityReference,
                                currentTask.sessionProfileId!!,
                                url,
                                cookies
                            )
                        } else {
                            Log.w("shsening", "No cookies found for URL: $url")
                        }
                    } catch (e: java.lang.Exception) {
                        Log.e("CookieHelper", "Failed to get cookies: ", e)
                    }
                }
            }
        }
        sessionProfileWebView.webChromeClient = object : WebChromeClient() {
            override fun onShowFileChooser(
                webView: WebView,
                filePathCallback: ValueCallback<Array<Uri>>,
                fileChooserParams: FileChooserParams
            ): Boolean {
                try {
                    var mFilePathCallback: ValueCallback<Array<Uri>>? = filePathCallback
                    // 选择特定的文件
                    val currentTask = uploadTaskQueue.peek()
                    val download_dir = File(getExternalFilesDir(null), currentTask.sessionProfileId)
                    val file = File(download_dir, currentTask.filename + ".mp4")
                    val uri: Uri =
                        FileProvider.getUriForFile(activityReference, AppConfig.FILE_PROVIDER, file)
                    mFilePathCallback?.onReceiveValue(arrayOf(uri))
                    return true
                }catch(e:Exception) {
                    return false
                }
            }
            override fun onShowCustomView(view: View?, callback: CustomViewCallback?) {
                super.onShowCustomView(view, callback)
                view?.requestFocus()
            }
        }
        sessionProfileWebView.isFocusableInTouchMode = true
        findViewById<Button>(R.id.btn_upload_back).setOnClickListener {
            finish()
        }
    }
    private fun processProfile(profile:SessionProfile){
        val downloadDir = File(getExternalFilesDir(null), profile.id) // 获取应用的外部存储目录,并指定 sessionid 子目录
        Utils.fetchTasksBySessionProfile(profile, object:Utils.FetchTasksCallback {
            override fun onTasksFetched(uploadTaskList: MutableList<Utils.VideoUploadTaskInfo>) {
                uploadTaskList.forEach { task ->
                    downloadHelper.downloadSingleFile(task.videoUrl, File(downloadDir,task.filename+".mp4"))
                    downloadHelper.downloadSingleFile(task.coverUrl, File(downloadDir,task.filename+".png"))
                    profile.getValidProfileNames().forEach{ platform ->
                        var taskInfo = Utils.VideoUploadTaskInfo(task.taskId,
                            task.title,
                            task.description,
                            task.datetime,
                            task.videoUrl,
                            task.coverUrl,
                            task.filename,
                            profile.id,
                            profile.name,
                            SESSION_PROFILE_PALTFORM_LISTS.find { it.name == platform }
                        )
                        uploadTaskQueue.offer(taskInfo)
                    }
                }
                Handler(Looper.getMainLooper()).postDelayed({
                    checkAndStartNextTask() // 延时等待下载任务
                }, 3000)
            }
        })
    }
    fun checkAndStartNextTask() {
        if (uploadTaskQueue.isNotEmpty()) {
            val nextTask = uploadTaskQueue.poll()
            nextTask?.let {
                nextTask.sessionProfileId?.let { sessionId ->
                    runOnUiThread {
                        if (last_uploaded_task_id != "" && last_uploaded_task_id != nextTask.taskId) {
                                Utils.sendVideoUploadedMessage(last_uploaded_task_id) { result, error ->
                                    Handler(Looper.getMainLooper()).post {
                                        Toast.makeText(
                                            activityReference,
                                            error?.message,
                                            Toast.LENGTH_SHORT
                                        )
                                    }
                            }
                        } else {
                            last_uploaded_task_id = nextTask.taskId
                        }
                        nextTask.platform?.let {
                                sessionProfileWebView.loadSessionProfileCookies(sessionId)
                                sessionProfileWebView.loadUrl(it.url)
                                Handler(Looper.getMainLooper()).post {
                                uploadSessionText.text = "任务:${nextTask.sessionProfileName}. 剩余:${uploadTaskQueue.size+1} "
                                }
                        };
                    }
                }
            }
        }else{
            Handler(Looper.getMainLooper()).post {
                uploadSessionText.text = "任务已结束。"
            }
            Handler(Looper.getMainLooper()).postDelayed({
                if (sessionProfiles != null) {
                    for (profile in sessionProfiles) {
                        updateSessionProfileLastUploadTime(profile)
                    }
                }
                finish()
            }, 2000)
        }
    }
    private fun updateSessionProfileLastUploadTime(sessionProfile: SessionProfile) {
        sessionProfile.lastUploadTime = Utils.getCurrentTimeString()
        sessionProfileManager.addOrUpdateSessionProfile(sessionProfile)
        sessionProfileManager.saveToFile()
    }
    override fun onDestroy() {
        super.onDestroy()
    }
}
app/src/main/java/com/shsening/autouploadvideos/WebviewUtils.kt
@@ -67,7 +67,5 @@
            }
    """.trimIndent()
        webView.evaluateJavascript(clickScript, null)
    }
}
app/src/main/java/com/shsening/autouploadvideos/action/ActionDouyin.kt
File was renamed from app/src/main/java/com/shsening/autouploadvideos/ActionDouyin.kt
@@ -1,15 +1,17 @@
package com.shsening.autouploadvideos
package com.shsening.autouploadvideos.action
import android.app.Instrumentation
import android.content.Context
import android.os.SystemClock
import android.os.Handler
import com.shsening.autouploadvideos.UploadSessionActivity
import com.shsening.autouploadvideos.utils.Utils
import com.shsening.autouploadvideos.WebviewUtils
class ActionDouyin (private var fragment:UploadFragment, private var context:Context, private var threadHandler: Handler, private var videoInfo:Utils.VideoUploadTaskInfo){
class ActionDouyin (private var activity: UploadSessionActivity, private var threadHandler: Handler, private var videoInfo: Utils.VideoUploadTaskInfo) :PlatformActionInterface{
    private var webView = fragment.webView
    private var webView = activity.sessionProfileWebView
    var jsCode_CheckContentLoaded = """
@@ -25,16 +27,39 @@
    private fun updatePostDescription(){
        WebviewUtils.addValueToInput(webView,"#root > div > div > div.content-body-v8tuJQ > div.form-kSES6A > div.publish-mention-wrapper-LWv5ed > div.editor-container-zRPSAi > div > div > div > div:nth-child(1) > div > div > input",videoInfo.title)
        WebviewUtils.addValueToInput(
            webView,
            "#root > div > div > div.content-body-v8tuJQ > div.form-kSES6A > div.publish-mention-wrapper-LWv5ed > div.editor-container-zRPSAi > div > div > div > div:nth-child(1) > div > div > input",
            videoInfo.title
        )
        WebviewUtils.addValueToDiv(webView,"#root > div > div > div.content-body-v8tuJQ > div.form-kSES6A > div.publish-mention-wrapper-LWv5ed > div.editor-container-zRPSAi > div > div > div > div.outerdocbody.editor-kit-outer-container > div",videoInfo.description)
        WebviewUtils.addValueToDiv(
            webView,
            "#root > div > div > div.content-body-v8tuJQ > div.form-kSES6A > div.publish-mention-wrapper-LWv5ed > div.editor-container-zRPSAi > div > div > div > div.outerdocbody.editor-kit-outer-container > div",
            videoInfo.description
        )
        WebviewUtils.simpleElementClick(webView,"#root > div > div > div.content-body-v8tuJQ > div.form-kSES6A > div.publish-mention-wrapper-LWv5ed > div:nth-child(3) > div > div.activity-list-container-AZzfdV > div:nth-child(1)")
        WebviewUtils.simpleElementClick(webView,"#root > div > div > div.content-body-v8tuJQ > div.form-kSES6A > div.publish-mention-wrapper-LWv5ed > div:nth-child(3) > div > div.activity-list-container-AZzfdV > div:nth-child(2)")
        WebviewUtils.simpleElementClick(
            webView,
            "#root > div > div > div.content-body-v8tuJQ > div.form-kSES6A > div.publish-mention-wrapper-LWv5ed > div:nth-child(3) > div > div.activity-list-container-AZzfdV > div:nth-child(1)"
        )
        WebviewUtils.simpleElementClick(
            webView,
            "#root > div > div > div.content-body-v8tuJQ > div.form-kSES6A > div.publish-mention-wrapper-LWv5ed > div:nth-child(3) > div > div.activity-list-container-AZzfdV > div:nth-child(2)"
        )
        WebviewUtils.simpleElementClick(webView,"#root > div > div > div.content-body-v8tuJQ > div.form-kSES6A > div.content-upload-new > div > div.recommendContainer-HXvL9W > div.recommendDisplay-ifUYki > div > div:nth-child(1) > div")
        WebviewUtils.simpleElementClick(webView,"#root > div > div > div.content-body-v8tuJQ > div.form-kSES6A > div.download-content-Lci5tL > div > label:nth-child(2)")
        WebviewUtils.simpleElementClick(webView,"#root > div > div > div.content-body-v8tuJQ > div.form-kSES6A > div.publish-settings-WtW_ZW > div.schedule-part-V615Zn > div.row-suTOx_ > label:nth-child(2)")
        WebviewUtils.simpleElementClick(
            webView,
            "#root > div > div > div.content-body-v8tuJQ > div.form-kSES6A > div.content-upload-new > div > div.recommendContainer-HXvL9W > div.recommendDisplay-ifUYki > div > div:nth-child(1) > div"
        )
        WebviewUtils.simpleElementClick(
            webView,
            "#root > div > div > div.content-body-v8tuJQ > div.form-kSES6A > div.download-content-Lci5tL > div > label:nth-child(2)"
        )
        WebviewUtils.simpleElementClick(
            webView,
            "#root > div > div > div.content-body-v8tuJQ > div.form-kSES6A > div.publish-settings-WtW_ZW > div.schedule-part-V615Zn > div.row-suTOx_ > label:nth-child(2)"
        )
        addValueToDatePickerInput(true,"#root > div > div > div.content-body-v8tuJQ > div.form-kSES6A > div.publish-settings-WtW_ZW > div.schedule-part-V615Zn > div.row-suTOx_ > div.date-picker-ioPchj > div > div",videoInfo.datetime)
@@ -45,7 +70,7 @@
    }
    fun checkIfContentLoaded() {
    override fun checkIfContentLoaded() {
        webView.postDelayed({
            webView.evaluateJavascript(jsCode_CheckContentLoaded) { result ->
                if (result == "true") {
@@ -60,7 +85,7 @@
    }
    private fun startToPublishVideo(){
        WebviewUtils.simpleElementClick(webView,jsPath_PublishVideo)
        WebviewUtils.simpleElementClick(webView, jsPath_PublishVideo)
        checkVideoPublishFinished()
    }
@@ -88,7 +113,7 @@
                if (result == "true") {
                    // video published successfully!!!
                    fragment.startUploadSingleTask()
                    activity.checkAndStartNextTask()
                } else {
@@ -108,7 +133,7 @@
        var dpPanelText = datetimes[0].toString()+"年 "+datetimes[1].toString()+"月";
        var dateRowColum = Utils.getDatePosition(datetimes[0],datetimes[1],datetimes[2])?:return;
        var dateRowColum = Utils.getDatePosition(datetimes[0], datetimes[1], datetimes[2]) ?:return;
        val jsCode = """
       try {
@@ -144,7 +169,7 @@
    private fun simulateSwipeAndUploadVideo() {
        val instrumentation = Instrumentation()
        val displayMetrics = context.resources.displayMetrics
        val displayMetrics = activity.resources.displayMetrics
        val width = displayMetrics.widthPixels
        val height = displayMetrics.heightPixels
app/src/main/java/com/shsening/autouploadvideos/action/ActionKuaishou.kt
File was renamed from app/src/main/java/com/shsening/autouploadvideos/ActionKuaishou.kt
@@ -1,15 +1,19 @@
package com.shsening.autouploadvideos
package com.shsening.autouploadvideos.action
import android.app.Instrumentation
import android.content.Context
import android.os.SystemClock
import android.os.Handler
import com.shsening.autouploadvideos.UploadFragment
import com.shsening.autouploadvideos.UploadSessionActivity
import com.shsening.autouploadvideos.utils.Utils
import com.shsening.autouploadvideos.WebviewUtils
class ActionKuaishou (private var fragment:UploadFragment, private var context:Context, private var threadHandler: Handler, private var videoInfo:Utils.VideoUploadTaskInfo){
class ActionKuaishou (private var activity: UploadSessionActivity, private var threadHandler: Handler, private var videoInfo: Utils.VideoUploadTaskInfo) :PlatformActionInterface{
    private var webView = fragment.webView
    private var webView = activity.sessionProfileWebView
    var jsCode_CheckContentLoaded = """
@@ -29,16 +33,25 @@
     */
    private fun updatePostDescription(){
        WebviewUtils.addValueToDiv(webView,"#work-description-edit",videoInfo.description)
        WebviewUtils.addValueToDiv(webView, "#work-description-edit", videoInfo.description)
        // do not allow download
        WebviewUtils.simpleElementClick(webView,"#setting-tours > div:nth-child(2) > div > label:nth-child(2)")
        WebviewUtils.simpleElementClick(
            webView,
            "#setting-tours > div:nth-child(2) > div > label:nth-child(2)"
        )
        // do not show same city
        WebviewUtils.simpleElementClick(webView,"#setting-tours > div:nth-child(2) > div > label:nth-child(3)")
        WebviewUtils.simpleElementClick(
            webView,
            "#setting-tours > div:nth-child(2) > div > label:nth-child(3)"
        )
        //Scheduled publish
        WebviewUtils.simpleElementClick(webView,"#setting-tours > div:nth-child(4) > div > div > label:nth-child(2)")
        WebviewUtils.simpleElementClick(
            webView,
            "#setting-tours > div:nth-child(4) > div > div > label:nth-child(2)"
        )
        addValueToDatePickerInput(true,"#setting-tours > div:nth-child(4) > div > div:nth-child(2) > div > div > input",videoInfo.datetime)
        // 在主线程中设置延迟执行
@@ -48,7 +61,7 @@
    }
    fun checkIfContentLoaded() {
    override fun checkIfContentLoaded() {
        webView.postDelayed({
            webView.evaluateJavascript(jsCode_CheckContentLoaded) { result ->
                if (result == "true") {
@@ -63,7 +76,7 @@
    }
    private fun startToPublishVideo(){
        WebviewUtils.simpleElementClick(webView,jsPath_PublishVideo)
        WebviewUtils.simpleElementClick(webView, jsPath_PublishVideo)
        checkVideoPublishFinished()
    }
    private fun checkVideoUploadFinished(){
@@ -87,7 +100,7 @@
            webView.evaluateJavascript(jsCode_PublishVideo_Finished) { result ->
                if (result == "true") {
                    // video published successfully!!!
                    fragment.startUploadSingleTask()
                    activity.checkAndStartNextTask()
                } else {
                    // 元素不存在,1秒后再次检查
                    webView.postDelayed({
@@ -103,9 +116,9 @@
        var datetimes = Utils.validateAndReturnDateTime(targetValue) ?: return
        var dpPanelText = datetimes[0].toString()+"年"+Utils.getMonthAbbreviation(datetimes[1]);
        var dpPanelText = datetimes[0].toString()+"年"+ Utils.getMonthAbbreviation(datetimes[1]);
        var dateRowColum = Utils.getDatePosition(datetimes[0],datetimes[1],datetimes[2])?:return;
        var dateRowColum = Utils.getDatePosition(datetimes[0], datetimes[1], datetimes[2]) ?:return;
        val jsCode = """
       try {
@@ -149,7 +162,7 @@
    private fun simulateSwipeAndUploadVideo() {
        val instrumentation = Instrumentation()
        val displayMetrics = context.resources.displayMetrics
        val displayMetrics = activity.resources.displayMetrics
        val width = displayMetrics.widthPixels
        val height = displayMetrics.heightPixels
@@ -160,7 +173,7 @@
        Thread {
            Utils.instrumentationSwipe(instrumentation, startX, y, endX, y)
            SystemClock.sleep(500)
            Utils.instrumentationClick(instrumentation, width *4/5f, height *2/ 3f)
            Utils.instrumentationClick(instrumentation, width * 4 / 5f, height * 2 / 3f)
            // back to main thread
            threadHandler.post {
                checkVideoUploadFinished()
app/src/main/java/com/shsening/autouploadvideos/action/ActionShipinhao.kt
File was renamed from app/src/main/java/com/shsening/autouploadvideos/ActionShipinhao.kt
@@ -1,15 +1,18 @@
package com.shsening.autouploadvideos
package com.shsening.autouploadvideos.action
import android.app.Instrumentation
import android.content.Context
import android.os.SystemClock
import android.os.Handler
import com.shsening.autouploadvideos.UploadFragment
import com.shsening.autouploadvideos.UploadSessionActivity
import com.shsening.autouploadvideos.utils.Utils
import com.shsening.autouploadvideos.WebviewUtils
class ActionShipinhao (private var fragment:UploadFragment, private var context:Context, private var threadHandler: Handler, private var videoInfo:Utils.VideoUploadTaskInfo){
class ActionShipinhao (private var activity: UploadSessionActivity, private var threadHandler: Handler, private var videoInfo: Utils.VideoUploadTaskInfo) :PlatformActionInterface{
    private var webView = fragment.webView
    private var webView = activity.sessionProfileWebView
    var jsCode_CheckContentLoaded = """
@@ -28,11 +31,18 @@
     * Title,description(including # and @ ) publish datetime etc
     */
    private fun updatePostDescription(){
        WebviewUtils.addValueToDiv(webView,"#container-wrap > div.container-center > div > div > div.main-body-wrap.post-create > div.main-body > div > div.post-edit-wrap.material-edit-wrap > div.form > div:nth-child(2) > div.form-item-body > div > div.input-editor",videoInfo.description)
        WebviewUtils.addValueToDiv(
            webView,
            "#container-wrap > div.container-center > div > div > div.main-body-wrap.post-create > div.main-body > div > div.post-edit-wrap.material-edit-wrap > div.form > div:nth-child(2) > div.form-item-body > div > div.input-editor",
            videoInfo.description
        )
        // publish later radiobox
        WebviewUtils.simpleElementClick(webView,"#container-wrap > div.container-center > div > div > div.main-body-wrap.post-create > div.main-body > div > div.post-edit-wrap.material-edit-wrap > div.form > div.post-time-wrap > div:nth-child(1) > div.form-item-body > div > label:nth-child(2)")
        WebviewUtils.simpleElementClick(
            webView,
            "#container-wrap > div.container-center > div > div > div.main-body-wrap.post-create > div.main-body > div > div.post-edit-wrap.material-edit-wrap > div.form > div.post-time-wrap > div:nth-child(1) > div.form-item-body > div > label:nth-child(2)"
        )
        addValueToDatePickerInput(true,"#container-wrap > div.container-center > div > div > div.main-body-wrap.post-create > div.main-body > div > div.post-edit-wrap.material-edit-wrap > div.form > div.post-time-wrap > div:nth-child(2) > div.form-item-body > dl > dd",videoInfo.datetime)
@@ -44,7 +54,7 @@
    }
    fun checkIfContentLoaded() {
    override fun checkIfContentLoaded() {
        webView.postDelayed({
            webView.evaluateJavascript(jsCode_CheckContentLoaded) { result ->
                if (result == "true") {
@@ -59,7 +69,7 @@
    }
    private fun startToPublishVideo(){
        WebviewUtils.simpleElementClick(webView,jsPath_PublishVideo)
        WebviewUtils.simpleElementClick(webView, jsPath_PublishVideo)
        checkVideoPublishFinished()
    }
    private fun checkVideoUploadFinished(){
@@ -83,7 +93,7 @@
            webView.evaluateJavascript(jsCode_PublishVideo_Finished) { result ->
                if (result == "true") {
                    // video published successfully!!!
                    fragment.startUploadSingleTask()
                    activity.checkAndStartNextTask()
                } else {
                    // 元素不存在,1秒后再次检查
                    webView.postDelayed({
@@ -101,7 +111,7 @@
        var dpPanelText = datetimes[0].toString()+"年 "+datetimes[1].toString()+"月";
        var dateRowColum = Utils.getDatePosition(datetimes[0],datetimes[1],datetimes[2])?:return;
        var dateRowColum = Utils.getDatePosition(datetimes[0], datetimes[1], datetimes[2]) ?:return;
        val jsCode = """
       try {
@@ -136,7 +146,7 @@
    private fun simulateSwipeAndUploadVideo() {
        val instrumentation = Instrumentation()
        val displayMetrics = context.resources.displayMetrics
        val displayMetrics = activity.resources.displayMetrics
        val width = displayMetrics.widthPixels
        val height = displayMetrics.heightPixels
@@ -147,7 +157,7 @@
        Thread {
            Utils.instrumentationSwipe(instrumentation, startX, y, endX, y)
            SystemClock.sleep(500)
            Utils.instrumentationClick(instrumentation, width *4/5f, height  / 2f)
            Utils.instrumentationClick(instrumentation, width * 4 / 5f, height / 2f)
            // back to main thread
            threadHandler.post {
                checkVideoUploadFinished()
app/src/main/java/com/shsening/autouploadvideos/action/PlatformActionInterface.kt
New file
@@ -0,0 +1,7 @@
package com.shsening.autouploadvideos.action
interface PlatformActionInterface {
    fun checkIfContentLoaded()
}
app/src/main/java/com/shsening/autouploadvideos/utils/BroadcastReceiver.kt
New file
@@ -0,0 +1,28 @@
package com.shsening.autouploadvideos.utils
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.widget.Toast
class DownloadReceiver(private val context: Context,private val downloadHelper: DownloadHelper) : BroadcastReceiver() {
    init {
        context.registerReceiver(this, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
    }
    override fun onReceive(context: Context, intent: Intent) {
        val action = intent.action
        if (DownloadManager.ACTION_DOWNLOAD_COMPLETE == action) {
            val downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
            if (downloadId != -1L) {
                // downoad finished
            }
        }
    }
    fun unregister() {
        context.unregisterReceiver(this)
    }
}
app/src/main/java/com/shsening/autouploadvideos/utils/DownloadHelper.kt
New file
@@ -0,0 +1,41 @@
package com.shsening.autouploadvideos.utils
import android.app.DownloadManager
import android.content.Context
import android.net.Uri
import android.os.Environment
import android.widget.Toast
import java.io.File
class DownloadHelper(private val context: Context) {
    private val downloadManager =
        context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
    fun downloadSingleFile(fileUrl:String,fileUri:File) {
            try {
                if (fileUri.exists()) {
                    return
                }
                // 确保目标文件所在的目录存在
                val parentDir = fileUri.parentFile
                if (parentDir != null && !parentDir.exists()) {
                    parentDir.mkdirs()  // 创建目录
                }
                val request = DownloadManager.Request(Uri.parse(fileUrl)).apply {
                    setTitle("Downloading ${fileUri.name}")
                    setDescription("Downloading file...")
                    .setDestinationUri(Uri.fromFile(fileUri)) // 设置下载文件保存路径
                    setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
                    setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE)
                }
                // 将下载请求加入队列
                downloadManager.enqueue(request)
            } catch (e: Exception) {
                e.printStackTrace()
                Toast.makeText(context, "Failed to download: $fileUrl", Toast.LENGTH_SHORT).show()
            }
    }
}
app/src/main/java/com/shsening/autouploadvideos/utils/Utils.kt
File was renamed from app/src/main/java/com/shsening/autouploadvideos/Utils.kt
@@ -1,13 +1,17 @@
package com.shsening.autouploadvideos
package com.shsening.autouploadvideos.utils
import SessionProfile
import android.app.Instrumentation
import android.content.Context
import android.content.Intent
import android.os.SystemClock
import android.view.MotionEvent
import android.webkit.CookieManager
import android.widget.Toast
import androidx.core.content.FileProvider
import com.shsening.autouploadvideos.AppConfig
import com.shsening.autouploadvideos.PlatformItem
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -16,11 +20,15 @@
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
import java.io.IOException
import java.io.Serializable
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
object Utils {
    data class AppUpdateInfo(
@@ -37,7 +45,10 @@
        val datetime: String,
        val videoUrl: String,
        val coverUrl: String,
        var filename: String = generateFilename(datetime)
        var filename: String = generateFilename(datetime),
        var sessionProfileId: String? =null,
        var sessionProfileName: String? =null,
        var platform: PlatformItem? =null
    ) : Serializable {
        companion object {
@@ -150,26 +161,37 @@
        return arrayOf(row, column) // 返回数组
    }
    suspend fun sendVideoUploadedMessage(context: Context,taskId: String): Boolean {
    fun sendVideoUploadedMessage(taskId: String, callback: (Boolean, Exception?) -> Unit) {
        try {
            val client = OkHttpClient()
            val request = Request.Builder()
                .url(AppConfig.URL_CONFIRM_TASK_UPLOADED + taskId)
                .build()
            // 异步执行网络请求
            client.newCall(request).enqueue(object : okhttp3.Callback {
                override fun onFailure(call: okhttp3.Call, e: IOException) {
                    // 请求失败,返回错误
                    callback(false, e)
                }
            return withContext(Dispatchers.IO) {
                val response = client.newCall(request).execute()
                if (!response.isSuccessful) throw IOException("Unexpected code $response")
                val json = JSONObject(response.body?.string() ?: "")
                json.getBoolean("result")
            }
        }catch(e:Exception){
                override fun onResponse(call: okhttp3.Call, response: okhttp3.Response) {
                    try {
                        val json = JSONObject(response.body?.string() ?: "")
                        val result = json.getString("msg")
                        // 成功回调
                        callback(true, Exception(result))
                    } catch (e: Exception) {
                        // 处理解析错误
                        callback(false, e)
                    } finally {
                        response.close()
                    }
                }
            })
        } catch (e: Exception) {
            e.printStackTrace()
            return false
            // 处理其他异常
            callback(false, e)
        }
    }
@@ -203,6 +225,56 @@
        }
    }
    interface FetchTasksCallback {
        fun onTasksFetched(uploadTaskList: MutableList<Utils.VideoUploadTaskInfo>)
    }
    interface UploadCallback {
        fun onSuccess()
        fun onError()
    }
    fun fetchTasksBySessionProfile(profile: SessionProfile, callback: FetchTasksCallback) {
        val uploadTaskList: MutableList<Utils.VideoUploadTaskInfo> = mutableListOf()
        val client = OkHttpClient()
        val request = Request.Builder()
            .url(AppConfig.URL_CHECK_CLIENT_TASKS + AppConfig.CLIENT_ANDROID_ID + "&sessionprofile=" + profile.id + "&videotype=" + profile.type) // 替换为你的任务地址
            .build()
        client.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                e.printStackTrace()
                // 可以在这里通知调用者出错
            }
            override fun onResponse(call: Call, response: Response) {
                response.use {
                    if (!response.isSuccessful) throw IOException("Unexpected code $response")
                    val jsonArray = JSONArray(response.body?.string() ?: "")
                    if (jsonArray.length() > 0) {
                        for (i in 0 until jsonArray.length()) {
                            val jsonObject = jsonArray.getJSONObject(i)
                            try {
                                val task = Utils.VideoUploadTaskInfo(
                                    taskId = jsonObject.getString("taskId"),
                                    title = jsonObject.getString("title"),
                                    description = jsonObject.getString("description"),
                                    datetime = jsonObject.getString("datetime"),
                                    videoUrl = jsonObject.getString("videoUrl"),
                                    coverUrl = jsonObject.getString("coverUrl")
                                )
                                uploadTaskList.add(task)
                            } catch (e: Exception) {
                                e.printStackTrace()
                            }
                        }
                    }
                }
                // 在请求成功并解析完任务列表后,调用回调函数
                callback.onTasksFetched(uploadTaskList)
            }
        })
    }
    suspend fun downloadCommonFileToExternalFilesDir(context: Context, url: String, filename: String, install: Boolean) {
        val tmpFile = File(context.getExternalFilesDir(null), filename)
@@ -220,18 +292,15 @@
                    override fun onFailure(call: Call, e: IOException) {
                        deferred.completeExceptionally(e) // 下载失败时标记为异常
                    }
                    override fun onResponse(call: Call, response: Response) {
                        response.use {
                            if (!response.isSuccessful) {
                                deferred.completeExceptionally(IOException("Unexpected code $response")) // 如果响应不成功,标记为异常
                                return
                            }
                            tmpFile.outputStream().use { outputStream ->
                                outputStream.write(response.body?.bytes())
                            }
                            if (install) {
                                installApk(context,tmpFile)
                            }
@@ -272,4 +341,24 @@
        return if (month in 1..12) months[month - 1] else "Invalid"
    }
    // 获取 key 的位置
    fun getSessionProfileValuePosition(key: String): Int {
        val index = AppConfig.SESSION_PROFILE_TYPE.values.toList().indexOf(key)
        return if (index == -1) 0 else index
    }
    fun getCurrentTimeString(): String {
        val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) // 自定义格式
        val currentDate = Date() // 获取当前时间
        return formatter.format(currentDate) // 格式化为字符串
    }
    // 根据位置获取 key
    fun getSessionProfileValueByPosition(position: Int?): String? {
        if (position==null){
            return "Unknown"
        }
        return AppConfig.SESSION_PROFILE_TYPE.values.toList().getOrElse(position) { "Unknown" }
    }
}
app/src/main/res/layout/activity_main.xml
@@ -33,7 +33,7 @@
            android:text="@string/app_tab_upload"/>
        <Button
            android:id="@+id/tabHelp"
            android:id="@+id/tabSetting"
            android:layout_width="0dp"
            android:layout_height="48dp"
            android:layout_weight="1"
app/src/main/res/layout/activity_session_profile.xml
New file
@@ -0,0 +1,102 @@
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:padding="6dp">
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <!-- 左边按钮:返回 -->
        <Button
            android:id="@+id/btn_back"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="返回"
            android:layout_alignParentStart="true" /> <!-- 靠左对齐 -->
        <Button
            android:id="@+id/btn_save"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="保存配置"
            android:layout_alignParentEnd="true" /> <!-- 靠右对齐 -->
    </RelativeLayout>
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView
            android:id="@+id/txt_session_id"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:gravity="center"/>
    </RelativeLayout>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:weightSum="2"
            android:padding="2dp">
            <TextView
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="Name:" />
            <EditText
                android:id="@+id/edit_name"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1" />
        </LinearLayout>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:weightSum="2"
            android:padding="2dp">
            <TextView
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="Type:" />
            <Spinner
                android:id="@+id/spinner_type"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1" />
        </LinearLayout>
        <LinearLayout
            android:id="@+id/switch_container"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:paddingTop="16dp" />
        <ProgressBar
            android:id="@+id/progressBar"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:visibility="gone" />
        <com.shsening.autouploadvideos.SessionProfileWebView
            android:id="@+id/sessionProfileWebView"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:focusable="true"
            android:focusableInTouchMode="true" />
</LinearLayout>
app/src/main/res/layout/fragment_help.xml
@@ -1,10 +1,44 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <WebView
        android:id="@+id/webView"
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>
        android:layout_height="wrap_content"
        android:padding="16dp">
        <TextView
            android:id="@+id/androidIdText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="18sp"
            android:layout_marginBottom="16dp" />
        <ImageView
            android:id="@+id/imageView"
            android:layout_width="300dp"
            android:layout_height="300dp"
            android:scaleType="centerCrop"
            android:layout_marginBottom="16dp" />
        <TextView
            android:id="@+id/descriptionText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="18sp"
            android:gravity="center"
            android:layout_marginBottom="16dp" />
        <!-- 使用固定高度代替 wrap_content -->
        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            <WebView
                android:id="@+id/webView"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />
        </FrameLayout>
    </LinearLayout>
</ScrollView>
app/src/main/res/layout/fragment_my.xml
File was deleted
app/src/main/res/layout/fragment_session_profile_list.xml
New file
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp">
    <Button
        android:id="@+id/btn_new"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:layout_marginEnd="16dp"
        android:text="新建" />
</RelativeLayout>
<ListView
    android:id="@+id/list_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:divider="@android:color/darker_gray"
    android:dividerHeight="1dp" />
</LinearLayout>
app/src/main/res/layout/list_item_with_checkbox.xml
New file
@@ -0,0 +1,48 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="8dp"
    android:gravity="center_vertical">
    <!-- 左侧的 CheckBox -->
    <CheckBox
        android:id="@+id/itemCheckBox"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:checked="true"
        android:layout_marginEnd="8dp" />
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:paddingEnd="8dp">
        <TextView
            android:id="@android:id/text1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="16sp"
            android:textColor="@android:color/black" />
        <TextView
            android:id="@android:id/text2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="14sp"
            android:textColor="@android:color/darker_gray" />
    </LinearLayout>
    <Button
        android:id="@+id/deleteButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:focusable="false"
        android:focusableInTouchMode="false"
        android:clickable="true"
        android:text="上传"
        android:textColor="@android:color/holo_blue_light"
        android:background="?android:attr/selectableItemBackground" />
</LinearLayout>
app/src/main/res/layout/list_item_with_delete.xml
New file
@@ -0,0 +1,40 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="8dp"
    android:gravity="center_vertical">
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:paddingEnd="8dp">
        <TextView
            android:id="@android:id/text1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="16sp"
            android:textColor="@android:color/black" />
        <TextView
            android:id="@android:id/text2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="14sp"
            android:textColor="@android:color/darker_gray" />
    </LinearLayout>
    <Button
        android:id="@+id/deleteButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:focusable="false"
        android:focusableInTouchMode="false"
        android:clickable="true"
        android:text="删除"
        android:textColor="@android:color/holo_blue_light"
        android:background="?android:attr/selectableItemBackground" />
</LinearLayout>
app/src/main/res/layout/upload_list.xml
New file
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
    <Button
        android:id="@+id/btn_all_task"
        android:layout_width="match_parent"
        android:layout_height="48dp"
        android:background="@color/teal_200"
        android:text="一键上传所有任务" />
<ListView
    android:id="@+id/upload_list_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:divider="@android:color/darker_gray"
    android:dividerHeight="1dp" />
</LinearLayout>
app/src/main/res/layout/upload_session.xml
New file
@@ -0,0 +1,41 @@
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:padding="6dp">
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center_vertical">
                <Button
                    android:id="@+id/btn_upload_back"
                    android:layout_width="wrap_content"
                    android:layout_height="48dp"
                    android:layout_alignParentStart="true"
                    android:gravity="center"
                    android:text="返回" />
                <TextView
                    android:id="@+id/uploadSessionText"
                    android:layout_width="250dp"
                    android:layout_height="48dp"
                    android:layout_alignParentRight="true"
                    android:background="@color/teal_200"
                    android:gravity="center" />
        </RelativeLayout>
        <com.shsening.autouploadvideos.SessionProfileWebView
            android:id="@+id/uploadSessionWebView"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:focusable="true"
            android:focusableInTouchMode="true" />
</LinearLayout>
app/src/main/res/values/strings.xml
@@ -2,8 +2,8 @@
    <string name="app_name">Video Upload</string>
    <string name="hello_blank_fragment">Hello blank fragment</string>
    <string name="app_tab_upload">上传视频</string>
    <string name="app_tab_description">使用说明</string>
    <string name="app_tab_my">我的</string>
    <string name="app_tab_description">模板设置</string>
    <string name="app_tab_my">使用说明</string>
    <string name="msg_douyin">抖音</string>
    <string name="msg_kuaishou">快手</string>
gradle/libs.versions.toml
@@ -12,7 +12,8 @@
appcompat="1.6.0"
constraintlayout="2.1.4"
material = "1.12.0"
parcelize="2.1.0"
firebaseStorageKtx = "21.0.1"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
@@ -32,9 +33,8 @@
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
firebase-storage-ktx = { group = "com.google.firebase", name = "firebase-storage-ktx", version.ref = "firebaseStorageKtx" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }