Ultra Commit
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# 默认忽略的文件
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 基于编辑器的 HTTP 客户端请求
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
1
.idea/gradle.xml
generated
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||||
<component name="GradleSettings">
|
<component name="GradleSettings">
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
|
|||||||
1
.idea/misc.xml
generated
@@ -1,4 +1,3 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<component name="FrameworkDetectionExcludesConfiguration">
|
<component name="FrameworkDetectionExcludesConfiguration">
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ plugins {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'org.astral.findmaimaiultra'
|
namespace 'org.astral.findmaimaiultra'
|
||||||
compileSdk 33
|
compileSdk 34
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "org.astral.findmaimaiultra"
|
applicationId "org.astral.findmaimaiultra"
|
||||||
minSdk 30
|
minSdk 30
|
||||||
targetSdk 33
|
targetSdk 34
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "1.0"
|
versionName "1.6.0 Ultra"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
@@ -72,4 +72,6 @@ dependencies {
|
|||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||||
|
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -36,6 +36,10 @@
|
|||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:enableOnBackInvokedCallback="true"
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:theme="@style/Theme.FindMaimaiUltra">
|
android:theme="@style/Theme.FindMaimaiUltra">
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="com.baidu.lbsapi.API_KEY"
|
||||||
|
android:value="lzzpL36kTbcmfQvhWDJOZwa3glQlYBbm"/>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.MainActivity"
|
android:name=".ui.MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -59,6 +63,20 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/Theme.FindMaimaiUltra.NoActionBar">
|
android:theme="@style/Theme.FindMaimaiUltra.NoActionBar">
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.PaikaActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:theme="@style/Theme.FindMaimaiUltra.NoActionBar">
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.UpdateActivity"
|
||||||
|
android:label="Update"
|
||||||
|
android:theme="@style/NewTheme"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:exported="true">
|
||||||
|
</activity>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package org.astral.findmaimaiultra.adapter;
|
||||||
|
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import com.bumptech.glide.Glide;
|
||||||
|
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||||
|
import com.bumptech.glide.request.RequestOptions;
|
||||||
|
import com.bumptech.glide.signature.ObjectKey;
|
||||||
|
import jp.wasabeef.glide.transformations.BlurTransformation;
|
||||||
|
import org.astral.findmaimaiultra.R;
|
||||||
|
import org.astral.findmaimaiultra.been.faker.MusicRating;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class MusicRatingAdapter extends RecyclerView.Adapter<MusicRatingAdapter.ViewHolder> {
|
||||||
|
|
||||||
|
private List<MusicRating> musicRatings;
|
||||||
|
private OnItemClickListener listener;
|
||||||
|
|
||||||
|
public interface OnItemClickListener {
|
||||||
|
void onItemClick(MusicRating musicRating);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnItemClickListener(OnItemClickListener listener) {
|
||||||
|
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MusicRatingAdapter(List<MusicRating> musicRatings) {
|
||||||
|
this.musicRatings = musicRatings;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_music_rating, parent, false);
|
||||||
|
return new ViewHolder(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||||
|
MusicRating musicRating = musicRatings.get(position);
|
||||||
|
holder.musicName.setText(musicRating.getMusicName());
|
||||||
|
holder.level.setText("Level " + musicRating.getLevel_info());
|
||||||
|
String ac = String.valueOf(musicRating.getAchievement());
|
||||||
|
//从后往前第5位加.
|
||||||
|
if(ac.length()>4) {
|
||||||
|
ac = ac.substring(0, ac.length() - 4) + "." + ac.substring(ac.length() - 4);
|
||||||
|
}
|
||||||
|
holder.ach.setText(ac);
|
||||||
|
int id = musicRating.getMusicId();
|
||||||
|
if (id > 10000) {
|
||||||
|
id = id - 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 假设 musicRating 有一个方法 getCoverImageUrl() 返回图片的URL
|
||||||
|
String imageUrl = "https://assets2.lxns.net/maimai/jacket/" + id + ".png";
|
||||||
|
if (imageUrl != null) {
|
||||||
|
RequestOptions options = new RequestOptions()
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.ALL) // 使用所有缓存策略
|
||||||
|
.signature(new ObjectKey(musicRating.getMusicId())) // 使用 MusicId 作为签名
|
||||||
|
.transform(new BlurTransformation(15, 1)); // 调整模糊半径为 15,采样因子为 1
|
||||||
|
|
||||||
|
Glide.with(holder.itemView.getContext())
|
||||||
|
.load(imageUrl)
|
||||||
|
.apply(options)
|
||||||
|
.into(holder.backgroundLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据 achievement 数据加载相应的图片
|
||||||
|
int achievement = musicRating.getAchievement();
|
||||||
|
int achievementImageResId = getAchievementImageResId(achievement);
|
||||||
|
if (achievementImageResId != 0) {
|
||||||
|
Glide.with(holder.itemView.getContext())
|
||||||
|
.load(achievementImageResId)
|
||||||
|
.into(holder.achievementImage);
|
||||||
|
} else {
|
||||||
|
holder.achievementImage.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.itemView.setOnClickListener(v -> {
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onItemClick(musicRating);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getAchievementImageResId(int achievement) {
|
||||||
|
// 根据 achievement 值返回相应的图片资源 ID
|
||||||
|
// 例如:
|
||||||
|
if(achievement > 1005000) {
|
||||||
|
return R.drawable.rank_sssp;
|
||||||
|
} else if (achievement > 1000000) {
|
||||||
|
return R.drawable.rank_sss;
|
||||||
|
} else if (achievement > 995000) {
|
||||||
|
return R.drawable.rank_ssp;
|
||||||
|
} else if (achievement > 990000) {
|
||||||
|
return R.drawable.rank_ss;
|
||||||
|
} else if (achievement > 980000) {
|
||||||
|
return R.drawable.rank_sp;
|
||||||
|
} else if (achievement > 970000) {
|
||||||
|
return R.drawable.rank_s;
|
||||||
|
} else if (achievement > 940000) {
|
||||||
|
return R.drawable.rank_aaa;
|
||||||
|
} else if (achievement > 900000) {
|
||||||
|
return R.drawable.rank_aaa;
|
||||||
|
} else if (achievement > 800000) {
|
||||||
|
return R.drawable.rank_a;
|
||||||
|
}
|
||||||
|
return R.drawable.rank_bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return musicRatings.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
TextView musicName;
|
||||||
|
TextView level;
|
||||||
|
TextView ach;
|
||||||
|
ImageView backgroundLayout;
|
||||||
|
ImageView achievementImage;
|
||||||
|
|
||||||
|
public ViewHolder(@NonNull View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
musicName = itemView.findViewById(R.id.musicName);
|
||||||
|
level = itemView.findViewById(R.id.level);
|
||||||
|
ach = itemView.findViewById(R.id.ach);
|
||||||
|
backgroundLayout = itemView.findViewById(R.id.backgroundLayout);
|
||||||
|
achievementImage = itemView.findViewById(R.id.achievementImage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package org.astral.findmaimaiultra.been.faker;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class MaiUser {
|
||||||
|
private int userId;
|
||||||
|
private int length;
|
||||||
|
private int nextIndex;
|
||||||
|
private List<UserMusicList> userMusicList;
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public int getUserId() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserId(int userId) {
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLength() {
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLength(int length) {
|
||||||
|
this.length = length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getNextIndex() {
|
||||||
|
return nextIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNextIndex(int nextIndex) {
|
||||||
|
this.nextIndex = nextIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<UserMusicList> getUserMusicList() {
|
||||||
|
return userMusicList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserMusicList(List<UserMusicList> userMusicList) {
|
||||||
|
this.userMusicList = userMusicList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "User{" +
|
||||||
|
"userId=" + userId +
|
||||||
|
", length=" + length +
|
||||||
|
", nextIndex=" + nextIndex +
|
||||||
|
", userMusicList=" + userMusicList +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,20 +3,74 @@ package org.astral.findmaimaiultra.been.faker;
|
|||||||
public class MusicRating {
|
public class MusicRating {
|
||||||
private int musicId;
|
private int musicId;
|
||||||
private String musicName;
|
private String musicName;
|
||||||
|
|
||||||
private int level;
|
private int level;
|
||||||
private double level_info;
|
private double level_info;
|
||||||
private int romVersion;
|
private int romVersion;
|
||||||
private int achievement;
|
private int achievement;
|
||||||
private int rating;
|
private int rating;
|
||||||
private String type;
|
private String type;
|
||||||
|
private int playCount;
|
||||||
|
private int comboStatus;
|
||||||
|
private int syncStatus;
|
||||||
|
private int deluxscoreMax;
|
||||||
|
private int scoreRank;
|
||||||
|
private int extNum1;
|
||||||
|
private int extNum2;
|
||||||
|
|
||||||
public String getType() {
|
public int getPlayCount() {
|
||||||
return type;
|
return playCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setType(String type) {
|
public void setPlayCount(int playCount) {
|
||||||
this.type = type;
|
this.playCount = playCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getComboStatus() {
|
||||||
|
return comboStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setComboStatus(int comboStatus) {
|
||||||
|
this.comboStatus = comboStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSyncStatus() {
|
||||||
|
return syncStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSyncStatus(int syncStatus) {
|
||||||
|
this.syncStatus = syncStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDeluxscoreMax() {
|
||||||
|
return deluxscoreMax;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDeluxscoreMax(int deluxscoreMax) {
|
||||||
|
this.deluxscoreMax = deluxscoreMax;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getScoreRank() {
|
||||||
|
return scoreRank;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setScoreRank(int scoreRank) {
|
||||||
|
this.scoreRank = scoreRank;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getExtNum1() {
|
||||||
|
return extNum1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExtNum1(int extNum1) {
|
||||||
|
this.extNum1 = extNum1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getExtNum2() {
|
||||||
|
return extNum2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExtNum2(int extNum2) {
|
||||||
|
this.extNum2 = extNum2;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getMusicName() {
|
public String getMusicName() {
|
||||||
@@ -27,6 +81,14 @@ public class MusicRating {
|
|||||||
this.musicName = musicName;
|
this.musicName = musicName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setType(String type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
public double getLevel_info() {
|
public double getLevel_info() {
|
||||||
return level_info;
|
return level_info;
|
||||||
}
|
}
|
||||||
@@ -72,6 +134,27 @@ public class MusicRating {
|
|||||||
return achievement;
|
return achievement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "MusicRating{" +
|
||||||
|
"musicId=" + musicId +
|
||||||
|
", musicName='" + musicName + '\'' +
|
||||||
|
", level=" + level +
|
||||||
|
", level_info=" + level_info +
|
||||||
|
", romVersion=" + romVersion +
|
||||||
|
", achievement=" + achievement +
|
||||||
|
", rating=" + rating +
|
||||||
|
", type='" + type + '\'' +
|
||||||
|
", playCount=" + playCount +
|
||||||
|
", comboStatus=" + comboStatus +
|
||||||
|
", syncStatus=" + syncStatus +
|
||||||
|
", deluxscoreMax=" + deluxscoreMax +
|
||||||
|
", scoreRank=" + scoreRank +
|
||||||
|
", extNum1=" + extNum1 +
|
||||||
|
", extNum2=" + extNum2 +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
|
||||||
public void setAchievement(int achievement) {
|
public void setAchievement(int achievement) {
|
||||||
this.achievement = achievement;
|
this.achievement = achievement;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package org.astral.findmaimaiultra.been.faker;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class UserMusicList {
|
||||||
|
private List<MusicRating> userMusicDetailList;
|
||||||
|
private int length;
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public List<MusicRating> getUserMusicDetailList() {
|
||||||
|
return userMusicDetailList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserMusicDetailList(List<MusicRating> userMusicDetailList) {
|
||||||
|
this.userMusicDetailList = userMusicDetailList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLength() {
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLength(int length) {
|
||||||
|
this.length = length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "UserMusicList{" +
|
||||||
|
"userMusicDetailList=" + userMusicDetailList +
|
||||||
|
", length=" + length +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package org.astral.findmaimaiultra.config;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import com.bumptech.glide.Glide;
|
||||||
|
import com.bumptech.glide.GlideBuilder;
|
||||||
|
import com.bumptech.glide.Registry;
|
||||||
|
import com.bumptech.glide.annotation.GlideModule;
|
||||||
|
import com.bumptech.glide.module.AppGlideModule;
|
||||||
|
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory;
|
||||||
|
import com.bumptech.glide.load.engine.cache.LruResourceCache;
|
||||||
|
import com.bumptech.glide.load.engine.cache.MemorySizeCalculator;
|
||||||
|
|
||||||
|
@GlideModule
|
||||||
|
public final class MyAppGlideModule extends AppGlideModule {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void applyOptions(Context context, GlideBuilder builder) {
|
||||||
|
// 设置内存缓存大小
|
||||||
|
MemorySizeCalculator.Builder calculator = new MemorySizeCalculator.Builder(context);
|
||||||
|
int defaultMemoryCacheSizeBytes = calculator.build().getMemoryCacheSize();
|
||||||
|
int customMemoryCacheSizeBytes = defaultMemoryCacheSizeBytes * 2; // 例如,设置为默认值的两倍
|
||||||
|
builder.setMemoryCache(new LruResourceCache(customMemoryCacheSizeBytes));
|
||||||
|
|
||||||
|
// 设置磁盘缓存大小
|
||||||
|
int diskCacheSizeBytes = 1024 * 1024 * 1024; // 1GB
|
||||||
|
builder.setDiskCache(new InternalCacheDiskCacheFactory(context, diskCacheSizeBytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerComponents(Context context, Glide glide, Registry registry) {
|
||||||
|
// 注册组件
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isManifestParsingEnabled() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
package org.astral.findmaimaiultra.ui;
|
package org.astral.findmaimaiultra.ui;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import com.bumptech.glide.Glide;
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
import com.google.android.material.navigation.NavigationView;
|
import com.google.android.material.navigation.NavigationView;
|
||||||
import androidx.navigation.NavController;
|
import androidx.navigation.NavController;
|
||||||
@@ -27,19 +30,12 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
setContentView(binding.getRoot());
|
setContentView(binding.getRoot());
|
||||||
|
|
||||||
setSupportActionBar(binding.appBarMain.toolbar);
|
setSupportActionBar(binding.appBarMain.toolbar);
|
||||||
binding.appBarMain.fab.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View view) {
|
|
||||||
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
|
|
||||||
.setAction("Action", null).show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
DrawerLayout drawer = binding.drawerLayout;
|
DrawerLayout drawer = binding.drawerLayout;
|
||||||
NavigationView navigationView = binding.navView;
|
NavigationView navigationView = binding.navView;
|
||||||
// Passing each menu ID as a set of Ids because each
|
// Passing each menu ID as a set of Ids because each
|
||||||
// menu should be considered as top level destinations.
|
// menu should be considered as top level destinations.
|
||||||
mAppBarConfiguration = new AppBarConfiguration.Builder(
|
mAppBarConfiguration = new AppBarConfiguration.Builder(
|
||||||
R.id.nav_home, R.id.nav_gallery, R.id.nav_slideshow)
|
R.id.nav_home, R.id.nav_gallery, R.id.nav_music,R.id.nav_slideshow)
|
||||||
.setOpenableLayout(drawer)
|
.setOpenableLayout(drawer)
|
||||||
.build();
|
.build();
|
||||||
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
|
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
|
||||||
@@ -51,7 +47,25 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
// Inflate the menu; this adds items to the action bar if it is present.
|
// Inflate the menu; this adds items to the action bar if it is present.
|
||||||
getMenuInflater().inflate(R.menu.main, menu);
|
getMenuInflater().inflate(R.menu.main, menu);
|
||||||
return true;
|
//点击效果
|
||||||
|
menu.findItem(R.id.action_settings).setOnMenuItemClickListener(item -> {
|
||||||
|
//切换到设置页面
|
||||||
|
Navigation.findNavController(this, R.id.nav_host_fragment_content_main).navigate(R.id.nav_slideshow);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
menu.findItem(R.id.action_paika).setOnMenuItemClickListener(item -> {
|
||||||
|
Intent paika = new Intent(this, PaikaActivity.class);
|
||||||
|
startActivity(paika);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
menu.findItem(R.id.action_update).setOnMenuItemClickListener(item -> {
|
||||||
|
Intent update = new Intent(this, UpdateActivity.class);
|
||||||
|
|
||||||
|
Toast.makeText(this, "敬请期待", Toast.LENGTH_SHORT).show();
|
||||||
|
//startActivity(update);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -0,0 +1,430 @@
|
|||||||
|
package org.astral.findmaimaiultra.ui;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.*;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
import com.bumptech.glide.Glide;
|
||||||
|
import com.google.android.material.button.MaterialButton;
|
||||||
|
import com.google.android.material.textfield.TextInputEditText;
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import okhttp3.*;
|
||||||
|
import org.astral.findmaimaiultra.R;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class PaikaActivity extends AppCompatActivity {
|
||||||
|
private MaterialButton enter;
|
||||||
|
private TextInputEditText partyName;
|
||||||
|
private Context context;
|
||||||
|
private OkHttpClient client;
|
||||||
|
private String party;
|
||||||
|
private TextView partyHouse;
|
||||||
|
private TableLayout tableLayout;
|
||||||
|
private SharedPreferences sharedPreferences;
|
||||||
|
private TextInputEditText name;
|
||||||
|
private String paikaname;
|
||||||
|
private MaterialButton add;
|
||||||
|
private MaterialButton play;
|
||||||
|
private MaterialButton leave;
|
||||||
|
private MaterialButton card;
|
||||||
|
private Handler handler;
|
||||||
|
private String cardStyle;
|
||||||
|
private String old = "";
|
||||||
|
@SuppressLint("MissingInflatedId")
|
||||||
|
@Override
|
||||||
|
protected void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
setContentView(R.layout.activity_paika);
|
||||||
|
enter = findViewById(R.id.enter);
|
||||||
|
partyName = findViewById(R.id.party);
|
||||||
|
context = getApplicationContext();
|
||||||
|
client = new OkHttpClient();
|
||||||
|
add = findViewById(R.id.add);
|
||||||
|
leave = findViewById(R.id.leave);
|
||||||
|
play = findViewById(R.id.play);
|
||||||
|
tableLayout = findViewById(R.id.tableLayout);
|
||||||
|
sharedPreferences = getSharedPreferences("setting", MODE_PRIVATE);
|
||||||
|
name = findViewById(R.id.name);
|
||||||
|
partyHouse= findViewById(R.id.partyHouse);
|
||||||
|
card = findViewById(R.id.card);
|
||||||
|
handler = new Handler();
|
||||||
|
if(sharedPreferences.getString("paikaname", null) != null) {
|
||||||
|
name.setText(sharedPreferences.getString("paikaname", null));
|
||||||
|
paikaname = sharedPreferences.getString("paikaname", null);
|
||||||
|
}
|
||||||
|
enter.setOnClickListener(v -> {
|
||||||
|
enterParty();
|
||||||
|
});
|
||||||
|
cardStyle = sharedPreferences.getString("cardStyle", "maimai PiNK.png");
|
||||||
|
if (!cardStyle.contains(".")){
|
||||||
|
cardStyle = cardStyle + ".png";
|
||||||
|
}
|
||||||
|
card.setText(cardStyle.split("\\.")[0]);
|
||||||
|
add.setOnClickListener(v -> {
|
||||||
|
paikaname = name.getText().toString();
|
||||||
|
if(paikaname.equals("")) {
|
||||||
|
Toast.makeText(context, "请输入昵称", Toast.LENGTH_SHORT).show();
|
||||||
|
} else {
|
||||||
|
updateCard();
|
||||||
|
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json"), "{\"party\":\"" + party + "\",\"name\":\"" + paikaname + "\"}");
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url("http://mai.godserver.cn:11451/api/mai/v1/party?party=" + party + "&people=" + paikaname)
|
||||||
|
.post(requestBody)
|
||||||
|
.build();
|
||||||
|
SharedPreferences.Editor editor = sharedPreferences.edit();
|
||||||
|
editor.putString("paikaname", paikaname);
|
||||||
|
editor.apply();
|
||||||
|
client.newCall(request).enqueue(new Callback() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
|
||||||
|
joinParty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call call, @NonNull IOException e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
leave.setOnClickListener(v->{
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url("http://mai.godserver.cn:11451/api/mai/v1/party?party=" + party + "&people=" + paikaname)
|
||||||
|
.delete()
|
||||||
|
.build();
|
||||||
|
play.setVisibility(View.GONE);
|
||||||
|
|
||||||
|
client.newCall(request).enqueue(new Callback() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
|
||||||
|
joinParty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call call, @NonNull IOException e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
play.setOnClickListener(v->{
|
||||||
|
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json"), "{\"party\":\"" + party + "\",\"people\":\"" + paikaname + "\"}");
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url("http://mai.godserver.cn:11451/api/mai/v1/partyPlay?party=" + party)
|
||||||
|
.post(requestBody)
|
||||||
|
.build();
|
||||||
|
client.newCall(request).enqueue(new Callback() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
joinParty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call call, @NonNull IOException e) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
card.setOnClickListener(v -> {
|
||||||
|
Toast.makeText(context, "目前样式:" + cardStyle, Toast.LENGTH_SHORT).show();
|
||||||
|
|
||||||
|
// 创建 AlertDialog
|
||||||
|
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||||
|
builder.setTitle("请选择卡牌样式");
|
||||||
|
|
||||||
|
// 加载自定义布局
|
||||||
|
View dialogView = getLayoutInflater().inflate(R.layout.dialog_card_style, null);
|
||||||
|
LinearLayout layout = dialogView.findViewById(R.id.layout_buttons); // 假设 LinearLayout 的 id 是 layout_buttons
|
||||||
|
|
||||||
|
// 添加选项
|
||||||
|
String[] styles = {
|
||||||
|
"maimai でらっくす FESTiVAL PLUS",
|
||||||
|
"maimai でらっくす UNiVERSE PLUS",
|
||||||
|
"maimai でらっくす PLUS",
|
||||||
|
"maimai でらっくす FESTiVAL",
|
||||||
|
"maimai でらっくす UNiVERSE",
|
||||||
|
"maimai でらっくす BUDDiES",
|
||||||
|
"maimai でらっくす",
|
||||||
|
"maimai MURASAKi",
|
||||||
|
"maimai PiNK",
|
||||||
|
"maimai ORANGE",
|
||||||
|
"maimai GreeN",
|
||||||
|
"maimai MiLK"
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建 AlertDialog 对象
|
||||||
|
AlertDialog dialog = builder.create();
|
||||||
|
|
||||||
|
for (String style : styles) {
|
||||||
|
MaterialButton button = new MaterialButton(this);
|
||||||
|
button.setText(style);
|
||||||
|
button.setPadding(16, 16, 16, 16);
|
||||||
|
button.setBackgroundColor(getResources().getColor(R.color.colorPrimary));
|
||||||
|
button.setTextColor(getResources().getColor(R.color.white));
|
||||||
|
button.setOnClickListener(view -> {
|
||||||
|
cardStyle = style + ".png";
|
||||||
|
updateCard();
|
||||||
|
Toast.makeText(context, "已选择样式: " + cardStyle, Toast.LENGTH_SHORT).show();
|
||||||
|
dialog.dismiss();
|
||||||
|
});
|
||||||
|
layout.addView(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置 AlertDialog 的内容视图
|
||||||
|
dialog.setView(dialogView);
|
||||||
|
|
||||||
|
// 显示 AlertDialog
|
||||||
|
dialog.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
private void updateCard() {
|
||||||
|
RequestBody emptyRequestBody = RequestBody.create("", MediaType.parse("text/plain"));
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url("http://mai.godserver.cn:11451/api/mai/v1/player?party=" + party + "&people=" + paikaname + "&card=" + cardStyle)
|
||||||
|
.post(emptyRequestBody)
|
||||||
|
.build();
|
||||||
|
card.setText(cardStyle.split("\\.")[0]);
|
||||||
|
SharedPreferences.Editor editor = sharedPreferences.edit();
|
||||||
|
editor.putString("cardStyle", cardStyle);
|
||||||
|
client.newCall(request).enqueue(new Callback() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
|
||||||
|
Log.d("TAG", "Response: " + response.body().string());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call call, @NonNull IOException e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
private void enterParty() {
|
||||||
|
LinearLayout joinParty = findViewById(R.id.joinParty);
|
||||||
|
joinParty.setVisibility(LinearLayout.VISIBLE);
|
||||||
|
LinearLayout enterParty = findViewById(R.id.enterParty);
|
||||||
|
enterParty.setVisibility(LinearLayout.GONE);
|
||||||
|
TextInputEditText partyName = findViewById(R.id.party);
|
||||||
|
party = partyName.getText().toString();
|
||||||
|
if (!name.getText().toString().isEmpty()) {
|
||||||
|
paikaname = name.getText().toString();
|
||||||
|
}
|
||||||
|
handler = new Handler();
|
||||||
|
Runnable runnable = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
joinParty();
|
||||||
|
handler.postDelayed(this, 5000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
handler.post(runnable);
|
||||||
|
}
|
||||||
|
private void joinParty() {
|
||||||
|
getShangJiPeople();
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url("http://mai.godserver.cn:11451/api/mai/v1/party?party=" + party)
|
||||||
|
.build();
|
||||||
|
Log.d("MainLaunch", "onResponse: " + request);
|
||||||
|
client.newCall(request).enqueue(new Callback() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
String responseData = null;
|
||||||
|
try {
|
||||||
|
responseData = response.body().string();
|
||||||
|
if(old.equals(responseData)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
String finalResponseData = responseData;
|
||||||
|
SharedPreferences.Editor editor = sharedPreferences.edit();
|
||||||
|
editor.putString("paikaname", paikaname);
|
||||||
|
editor.commit();
|
||||||
|
runOnUiThread(() -> {
|
||||||
|
List<String> list = new Gson().fromJson(finalResponseData, new TypeToken<List<String>>() {
|
||||||
|
}.getType());
|
||||||
|
tableLayout.removeAllViews();
|
||||||
|
|
||||||
|
// 表头行
|
||||||
|
TableRow headerRow = new TableRow(context);
|
||||||
|
addTextViewToRow(headerRow, "排卡顺序", 2);
|
||||||
|
addTextViewToRow(headerRow, "昵称", 5);
|
||||||
|
addTextViewToRow(headerRow, "辅助操作此玩家", 4);
|
||||||
|
tableLayout.addView(headerRow);
|
||||||
|
|
||||||
|
// 数据行
|
||||||
|
for (int i = 0; i < list.size(); i++) {
|
||||||
|
String name = list.get(i);
|
||||||
|
if (i == 0) {
|
||||||
|
play.setVisibility(name.equals(paikaname) ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
TableRow row = new TableRow(context);
|
||||||
|
|
||||||
|
// 排卡顺序
|
||||||
|
addTextViewToRow(row, (i + 1) + "", 1);
|
||||||
|
|
||||||
|
// 昵称(图片 + 文字)
|
||||||
|
LinearLayout nameLayout = new LinearLayout(context);
|
||||||
|
nameLayout.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
TableRow.LayoutParams layoutParams = new TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 5);
|
||||||
|
nameLayout.setLayoutParams(layoutParams);
|
||||||
|
|
||||||
|
// 图片
|
||||||
|
ImageView imageView = new ImageView(context);
|
||||||
|
updateUserCard( imageView,name);
|
||||||
|
nameLayout.addView(imageView);
|
||||||
|
|
||||||
|
// 文字
|
||||||
|
TextView textView = new TextView(context);
|
||||||
|
textView.setText(name);
|
||||||
|
textView.setTextSize(14);
|
||||||
|
textView.setPadding(30, 20, 0, 0);
|
||||||
|
//设置颜色colorPrimary
|
||||||
|
//设计成斜式
|
||||||
|
textView.setTypeface(Typeface.create("serif-italic", Typeface.NORMAL));
|
||||||
|
textView.setTextColor(getResources().getColor(R.color.colorSecondary));
|
||||||
|
nameLayout.addView(textView);
|
||||||
|
|
||||||
|
row.addView(nameLayout);
|
||||||
|
|
||||||
|
// 辅助操作按钮
|
||||||
|
addButtonToRow(row, "插队", 1, name);
|
||||||
|
addButtonToRow(row, "上机", 1, name);
|
||||||
|
addButtonToRow(row, "离开", 1, name);
|
||||||
|
|
||||||
|
tableLayout.addView(row);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
old = finalResponseData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call call, @NonNull IOException e) {
|
||||||
|
Log.e("MainLaunch", "onFailure: ", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
private void updateUserCard(ImageView imageView,String people) {
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url("http://mai.godserver.cn:11451/api/mai/v1/player?people=" + people + "&party=" + party)
|
||||||
|
.build();
|
||||||
|
client.newCall(request).enqueue(new Callback() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
|
||||||
|
if(response.isSuccessful()) {
|
||||||
|
String responseData = response.body().string();
|
||||||
|
runOnUiThread(()->{
|
||||||
|
Glide.with(context)
|
||||||
|
.load("http://cdn.godserver.cn/resource/static/mai/pic/" + responseData) // 图片URL
|
||||||
|
.placeholder(R.drawable.placeholder) // 占位图
|
||||||
|
.into(imageView);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call call, @NonNull IOException e) {
|
||||||
|
Toast.makeText(context, "卡牌展示失败", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 辅助方法:添加 TextView 到 TableRow 并设置权重
|
||||||
|
private void addTextViewToRow(TableRow row, String text, int weight) {
|
||||||
|
TextView textView = new TextView(context);
|
||||||
|
textView.setText(text);
|
||||||
|
textView.setTextColor(ContextCompat.getColor(context, R.color.textcolorPrimary));
|
||||||
|
TableRow.LayoutParams params = new TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, weight);
|
||||||
|
textView.setLayoutParams(params);
|
||||||
|
row.addView(textView);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助方法:添加 Button 到 TableRow 并设置权重
|
||||||
|
private void addButtonToRow(TableRow row, String text, int weight,String username) {
|
||||||
|
Button button = new Button(context);
|
||||||
|
button.setText(text);
|
||||||
|
if(text.equals("插队")) {
|
||||||
|
addButton(button,"change",paikaname,username);
|
||||||
|
}else if(text.equals("上机")) {
|
||||||
|
addButton(button,"play",paikaname,username);
|
||||||
|
}else if(text.equals("离开")) {
|
||||||
|
addButton(button,"leave",paikaname,username);
|
||||||
|
}
|
||||||
|
TableRow.LayoutParams params = new TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, weight);
|
||||||
|
button.setLayoutParams(params);
|
||||||
|
row.addView(button);
|
||||||
|
}
|
||||||
|
private void addButton(Button button,String data1,String data0,String data) {
|
||||||
|
button.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View view) {
|
||||||
|
Request request = null;
|
||||||
|
if(data1.equals("change")) {
|
||||||
|
RequestBody body = RequestBody.create(data1, MediaType.get("application/json; charset=utf-8"));
|
||||||
|
request = new Request.Builder()
|
||||||
|
.url("http://mai.godserver.cn:11451/api/mai/v1/party?party=" + party + "&people=" + data0 + "&changeToPeople=" + data)
|
||||||
|
.put(body)
|
||||||
|
.build();
|
||||||
|
}else if(data1.equals("play")) {
|
||||||
|
RequestBody body = RequestBody.create(data1, MediaType.get("application/json; charset=utf-8"));
|
||||||
|
request = new Request.Builder()
|
||||||
|
.url("http://mai.godserver.cn:11451/api/mai/v1/partyPlay?party=" + party + "&people=" + data)
|
||||||
|
.delete(body)
|
||||||
|
.build();
|
||||||
|
}else if(data1.equals("leave")) {
|
||||||
|
RequestBody body = RequestBody.create(data1, MediaType.get("application/json; charset=utf-8"));
|
||||||
|
request = new Request.Builder()
|
||||||
|
.url("http://mai.godserver.cn:11451/api/mai/v1/party?party=" + party + "&people=" + data)
|
||||||
|
.delete(body)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
client.newCall(request).enqueue(new Callback() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
|
||||||
|
Log.d("TAG", "onResponse: " + response.body().string());
|
||||||
|
runOnUiThread(()->{
|
||||||
|
Toast.makeText(PaikaActivity.this, "操作成功", Toast.LENGTH_SHORT);
|
||||||
|
});
|
||||||
|
joinParty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call call, @NonNull IOException e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public void getShangJiPeople() {
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url("http://mai.godserver.cn:11451/api/mai/v1/partyPlay?party=" + party)
|
||||||
|
.build();
|
||||||
|
client.newCall(request).enqueue(new Callback() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
|
||||||
|
String data = response.body().string();
|
||||||
|
runOnUiThread(()->{
|
||||||
|
partyHouse.setText("房间号"+ party+" "+data + "正在上机");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call call, @NonNull IOException e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,610 @@
|
|||||||
|
package org.astral.findmaimaiultra.ui;
|
||||||
|
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.*;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.icu.util.Calendar;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.text.method.ScrollingMovementMethod;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.*;
|
||||||
|
import androidx.appcompat.app.ActionBar;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.appcompat.widget.SwitchCompat;
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.stream.JsonWriter;
|
||||||
|
import okhttp3.*;
|
||||||
|
import org.astral.findmaimaiultra.R;
|
||||||
|
import org.astral.findmaimaiultra.been.PlayerData;
|
||||||
|
import org.astral.findmaimaiultra.been.lx.Lx_chart;
|
||||||
|
import org.astral.findmaimaiultra.utill.Shuiyu2Luoxue;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.crawler.Callback;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.crawler.CrawlerCaller;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.notification.NotificationUtil;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.server.HttpServer;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.server.HttpServerService;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.ui.DataContext;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.core.Constant;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.core.LocalVpnService;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.core.ProxyConfig;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
import static org.astral.findmaimaiultra.utill.updater.Util.copyText;
|
||||||
|
import static org.astral.findmaimaiultra.utill.updater.Util.getDifficulties;
|
||||||
|
import static org.astral.findmaimaiultra.utill.updater.crawler.CrawlerCaller.writeLog;
|
||||||
|
|
||||||
|
public class UpdateActivity extends AppCompatActivity implements
|
||||||
|
CompoundButton.OnCheckedChangeListener,
|
||||||
|
LocalVpnService.onStatusChangedListener {
|
||||||
|
|
||||||
|
private static final String TAG = UpdateActivity.class.getSimpleName();
|
||||||
|
private static final int START_VPN_SERVICE_REQUEST_CODE = 1985;
|
||||||
|
private static String GL_HISTORY_LOGS;
|
||||||
|
private SwitchCompat switchProxy;
|
||||||
|
private TextView textViewLog;
|
||||||
|
private ScrollView scrollViewLog;
|
||||||
|
private Calendar mCalendar;
|
||||||
|
|
||||||
|
private SharedPreferences mContextSp;
|
||||||
|
private Context context = this;
|
||||||
|
private void updateTilte() {
|
||||||
|
ActionBar actionBar = getSupportActionBar();
|
||||||
|
if (actionBar != null) {
|
||||||
|
if (LocalVpnService.IsRunning) {
|
||||||
|
actionBar.setTitle(getString(R.string.connected));
|
||||||
|
} else {
|
||||||
|
actionBar.setTitle(getString(R.string.disconnected));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressLint("MissingInflatedId")
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_update);
|
||||||
|
|
||||||
|
androidx.appcompat.widget.Toolbar toolbar = (androidx.appcompat.widget.Toolbar) findViewById(R.id.toolbar);
|
||||||
|
setSupportActionBar(toolbar);
|
||||||
|
|
||||||
|
textViewLog = (TextView) findViewById(R.id.textViewLog);
|
||||||
|
|
||||||
|
assert textViewLog != null;
|
||||||
|
textViewLog.setText(GL_HISTORY_LOGS);
|
||||||
|
textViewLog.setMovementMethod(ScrollingMovementMethod.getInstance());
|
||||||
|
mCalendar = Calendar.getInstance();
|
||||||
|
LocalVpnService.addOnStatusChangedListener(this);
|
||||||
|
|
||||||
|
mContextSp = this.getSharedPreferences(
|
||||||
|
"updater.data",
|
||||||
|
Context.MODE_PRIVATE);
|
||||||
|
|
||||||
|
CrawlerCaller.listener = this;
|
||||||
|
|
||||||
|
loadContextData();
|
||||||
|
|
||||||
|
Button sy2lx = findViewById(R.id.sy2lx);
|
||||||
|
sy2lx.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View view) {
|
||||||
|
// 创建 OkHttpClient 实例
|
||||||
|
OkHttpClient client = new OkHttpClient();
|
||||||
|
final SharedPreferences sp = getSharedPreferences("setting", Context.MODE_PRIVATE);
|
||||||
|
// 原始数据
|
||||||
|
String rawData = "{\"username\":\"" + sp.getString("shuiyu_username", "") + "\",\"b50\":true}";
|
||||||
|
RequestBody body = RequestBody.create(rawData, MediaType.get("application/json; charset=utf-8"));
|
||||||
|
// 创建 Request
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url("https://www.diving-fish.com/api/maimaidxprober/query/player")
|
||||||
|
.post(body)
|
||||||
|
.build();
|
||||||
|
// 使用 AsyncTask 发送请求
|
||||||
|
new SendRequestTask(client, request,0).execute();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
private class SendRequestTask extends AsyncTask<Void, Void, String> {
|
||||||
|
private OkHttpClient client;
|
||||||
|
private Request request;
|
||||||
|
private int t;
|
||||||
|
|
||||||
|
public SendRequestTask(OkHttpClient client, Request request,int t) {
|
||||||
|
this.client = client;
|
||||||
|
this.request = request;
|
||||||
|
this.t =t;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String doInBackground(Void... voids) {
|
||||||
|
try (Response response = client.newCall(request).execute()) {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
return response.body().string();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(String result) {
|
||||||
|
if (result == null) {
|
||||||
|
Toast.makeText(context, "请求失败", Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(this.t==0) {
|
||||||
|
// 使用Gson进行反序列化
|
||||||
|
Gson gson = new Gson();
|
||||||
|
PlayerData playerData = gson.fromJson(result, PlayerData.class);
|
||||||
|
ArrayList<Lx_chart> lx_charts = Shuiyu2Luoxue.shuiyu2luoxue(playerData);
|
||||||
|
// 分割 lx_charts 列表
|
||||||
|
|
||||||
|
// 分割 lx_charts 列表成五个部分
|
||||||
|
int size = lx_charts.size();
|
||||||
|
int partSize = (int) Math.ceil(size / 2.0);
|
||||||
|
|
||||||
|
List<List<Lx_chart>> parts = new ArrayList<>();
|
||||||
|
for (int i = 0; i < 2; i++) {
|
||||||
|
int fromIndex = i * partSize;
|
||||||
|
int toIndex = Math.min(fromIndex + partSize, size);
|
||||||
|
parts.add(lx_charts.subList(fromIndex, toIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 处理每个部分
|
||||||
|
for (int i = 0; i < parts.size(); i++) {
|
||||||
|
String rawPart = serializeToJson(gson, parts.get(i));
|
||||||
|
rawPart = "{\"scores\":" + rawPart + "}";
|
||||||
|
Log.d("rawPart", rawPart);
|
||||||
|
OkHttpClient client = new OkHttpClient();
|
||||||
|
RequestBody body = RequestBody.create(rawPart, MediaType.get("application/json; charset=utf-8"));
|
||||||
|
String code = getSharedPreferences("setting", Context.MODE_PRIVATE).getString("luoxue_username","");
|
||||||
|
// 创建 Request
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url("https://maimai.lxns.net/api/v0/user/maimai/player/scores")
|
||||||
|
.header("X-User-Token",code) // 添加认证头
|
||||||
|
.post(body)
|
||||||
|
.build();
|
||||||
|
// 使用 AsyncTask 发送请求
|
||||||
|
new SendRequestTask(client, request,1).execute();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(this.t==1) {
|
||||||
|
Log.d("out",result);
|
||||||
|
Toast.makeText(context, "上传成功,数据已从水鱼传到落雪~", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
// 这里raw是发送给落雪查分器的数据,代表着上传歌曲信息
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private void inputAddress() {
|
||||||
|
// AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||||
|
// builder.setTitle("Http com.bakapiano.maimai.com.bakapiano.maimai.proxy server");
|
||||||
|
// final EditText input = new EditText(this);
|
||||||
|
// input.setText(ProxyConfig.getHttpProxyServer(this));
|
||||||
|
// builder.setView(input);
|
||||||
|
// builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
|
||||||
|
// @Override
|
||||||
|
// public void onClick(DialogInterface dialog, int which) {
|
||||||
|
// String text = input.getText().toString();
|
||||||
|
// ProxyConfig.Instance.setProxy(text);
|
||||||
|
// ProxyConfig.setHttpProxyServer(MainActivity.this, text);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// builder.setCancelable(false);
|
||||||
|
// builder.show();
|
||||||
|
@SuppressLint("AuthLeak") String text = "http://user:pass@127.0.0.1:8848";
|
||||||
|
ProxyConfig.Instance.setProxy(text);
|
||||||
|
ProxyConfig.setHttpProxyServer(UpdateActivity.this, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
updateTilte();
|
||||||
|
}
|
||||||
|
|
||||||
|
String getVersionName() {
|
||||||
|
PackageManager packageManager = getPackageManager();
|
||||||
|
if (packageManager == null) {
|
||||||
|
Log.e(TAG, "null package manager is impossible");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return packageManager.getPackageInfo(getPackageName(), 0).versionName;
|
||||||
|
} catch (PackageManager.NameNotFoundException e) {
|
||||||
|
Log.e(TAG, "package not found is impossible", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("DefaultLocale")
|
||||||
|
@Override
|
||||||
|
public void onLogReceived(String logString) {
|
||||||
|
mCalendar.setTimeInMillis(System.currentTimeMillis());
|
||||||
|
logString = String.format("[%1$02d:%2$02d:%3$02d] %4$s\n",
|
||||||
|
mCalendar.get(Calendar.HOUR_OF_DAY),
|
||||||
|
mCalendar.get(Calendar.MINUTE),
|
||||||
|
mCalendar.get(Calendar.SECOND),
|
||||||
|
logString);
|
||||||
|
|
||||||
|
Log.d(Constant.TAG, logString);
|
||||||
|
|
||||||
|
textViewLog.append(logString);
|
||||||
|
GL_HISTORY_LOGS = textViewLog.getText() == null ? "" : textViewLog.getText().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStatusChanged(String status, Boolean isRunning) {
|
||||||
|
switchProxy.setEnabled(true);
|
||||||
|
switchProxy.setChecked(isRunning);
|
||||||
|
onLogReceived(status);
|
||||||
|
updateTilte();
|
||||||
|
Toast.makeText(this, status, Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
private static String serializeToJson(Gson gson, List<Lx_chart> charts) throws IOException {
|
||||||
|
StringWriter stringWriter = new StringWriter();
|
||||||
|
JsonWriter jsonWriter = gson.newJsonWriter(stringWriter);
|
||||||
|
|
||||||
|
try {
|
||||||
|
jsonWriter.beginArray();
|
||||||
|
for (Lx_chart chart : charts) {
|
||||||
|
gson.toJson(chart, Lx_chart.class, jsonWriter);
|
||||||
|
}
|
||||||
|
jsonWriter.endArray();
|
||||||
|
jsonWriter.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
return stringWriter.toString();
|
||||||
|
}
|
||||||
|
private final Object switchLock = new Object();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||||
|
if (!switchProxy.isEnabled()) return;
|
||||||
|
if (!switchProxy.isPressed()) return;
|
||||||
|
saveOptions();
|
||||||
|
saveDifficulties();
|
||||||
|
|
||||||
|
if (getDifficulties().isEmpty()) {
|
||||||
|
if (isChecked) {
|
||||||
|
writeLog("请至少勾选一个难度!");
|
||||||
|
}
|
||||||
|
switchProxy.setChecked(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Context context = this;
|
||||||
|
if (LocalVpnService.IsRunning != isChecked) {
|
||||||
|
switchProxy.setEnabled(false);
|
||||||
|
if (isChecked) {
|
||||||
|
NotificationUtil.getINSTANCE().setContext(this).startNotification();
|
||||||
|
checkProberAccount(result -> {
|
||||||
|
this.runOnUiThread(() -> {
|
||||||
|
if ((Boolean) result) {
|
||||||
|
// getAuthLink(link -> {
|
||||||
|
if (DataContext.CopyUrl) {
|
||||||
|
String link = DataContext.WebHost;
|
||||||
|
// Use local auth server if web host is not set
|
||||||
|
if (link.length() == 0) {
|
||||||
|
link = "http://127.0.0.2:" + HttpServer.Port + "/" + getRandomString(10);
|
||||||
|
}
|
||||||
|
String finalLink = link;
|
||||||
|
this.runOnUiThread(() -> copyText(context, finalLink));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start vpn service
|
||||||
|
Intent intent = LocalVpnService.prepare(context);
|
||||||
|
if (intent == null) {
|
||||||
|
startVPNService();
|
||||||
|
// Jump to wechat app
|
||||||
|
if (DataContext.AutoLaunch) {
|
||||||
|
getWechatApi();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
startActivityForResult(intent, START_VPN_SERVICE_REQUEST_CODE);
|
||||||
|
}
|
||||||
|
// Start http service
|
||||||
|
startHttpService();
|
||||||
|
// });
|
||||||
|
} else {
|
||||||
|
switchProxy.setChecked(false);
|
||||||
|
switchProxy.setEnabled(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
LocalVpnService.IsRunning = false;
|
||||||
|
stopHttpService();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startHttpService() {
|
||||||
|
startService(new Intent(this, HttpServerService.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopHttpService() {
|
||||||
|
stopService(new Intent(this, HttpServerService.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startVPNService() {
|
||||||
|
textViewLog.setText("");
|
||||||
|
GL_HISTORY_LOGS = null;
|
||||||
|
onLogReceived("starting...");
|
||||||
|
startService(new Intent(this, LocalVpnService.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
|
||||||
|
if (requestCode == START_VPN_SERVICE_REQUEST_CODE) {
|
||||||
|
if (resultCode == RESULT_OK) {
|
||||||
|
startVPNService();
|
||||||
|
// Jump to wechat app
|
||||||
|
getWechatApi();
|
||||||
|
} else {
|
||||||
|
switchProxy.setChecked(false);
|
||||||
|
switchProxy.setEnabled(true);
|
||||||
|
onLogReceived("canceled.");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onActivityResult(requestCode, resultCode, intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
|
getMenuInflater().inflate(R.menu.main_activity_vpnactions, menu);
|
||||||
|
|
||||||
|
MenuItem menuItem = menu.findItem(R.id.menu_item_switch);
|
||||||
|
if (menuItem == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switchProxy = (SwitchCompat) menuItem.getActionView();
|
||||||
|
if (switchProxy == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switchProxy.setChecked(LocalVpnService.IsRunning);
|
||||||
|
switchProxy.setOnCheckedChangeListener(this);
|
||||||
|
|
||||||
|
if (!switchProxy.isChecked()) {
|
||||||
|
inputAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
LocalVpnService.removeOnStatusChangedListener(this);
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveText(View view) {
|
||||||
|
Context context = this;
|
||||||
|
checkProberAccount(result -> {
|
||||||
|
if ((Boolean) result) {
|
||||||
|
saveContextData();
|
||||||
|
this.runOnUiThread(() -> {
|
||||||
|
new AlertDialog.Builder(context)
|
||||||
|
.setTitle(getString(R.string.app_name) + " " + getVersionName())
|
||||||
|
.setMessage("查分器账户保存成功")
|
||||||
|
.setPositiveButton(R.string.btn_ok, null)
|
||||||
|
.show();
|
||||||
|
SharedPreferences sp = getSharedPreferences("setting", Context.MODE_PRIVATE);
|
||||||
|
@SuppressLint("CommitPrefEdits") SharedPreferences.Editor editor = sp.edit();
|
||||||
|
TextView password = findViewById(R.id.password);
|
||||||
|
TextView username = findViewById(R.id.username);
|
||||||
|
editor.putString("shuiyu_password", password.getText().toString());
|
||||||
|
editor.putString("shuiyu_username", username.getText().toString());
|
||||||
|
editor.commit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showInvalidAccountDialog() {
|
||||||
|
this.runOnUiThread(() -> {
|
||||||
|
new AlertDialog.Builder(this)
|
||||||
|
.setTitle(getString(R.string.app_name) + " " + getVersionName())
|
||||||
|
.setMessage("查分账户信息无效")
|
||||||
|
.setPositiveButton(R.string.btn_ok, null)
|
||||||
|
.show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openWebLink(String url) {
|
||||||
|
Intent intent = new Intent();
|
||||||
|
intent.setData(Uri.parse(url));
|
||||||
|
intent.setAction(Intent.ACTION_VIEW);
|
||||||
|
this.startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getAuthLink(Callback callback) {
|
||||||
|
new Thread() {
|
||||||
|
public void run() {
|
||||||
|
String link = CrawlerCaller.getWechatAuthUrl();
|
||||||
|
callback.onResponse(link);
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getLatestVersion(Callback callback) {
|
||||||
|
CrawlerCaller.getLatestVersion(result -> {
|
||||||
|
String version = (String)result;
|
||||||
|
callback.onResponse(version);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkProberAccount(Callback callback) {
|
||||||
|
DataContext.Username = ((TextView) findViewById(R.id.username)).getText().toString();
|
||||||
|
DataContext.Password = ((TextView) findViewById(R.id.password)).getText().toString();
|
||||||
|
|
||||||
|
saveOptions();
|
||||||
|
|
||||||
|
saveDifficulties();
|
||||||
|
|
||||||
|
if (DataContext.Username == null || DataContext.Password == null) {
|
||||||
|
showInvalidAccountDialog();
|
||||||
|
callback.onResponse(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CrawlerCaller.verifyAccount(DataContext.Username, DataContext.Password, result -> {
|
||||||
|
if (!(Boolean) result) showInvalidAccountDialog();
|
||||||
|
callback.onResponse(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveDifficulties() {
|
||||||
|
DataContext.BasicEnabled = ((CheckBox) findViewById(R.id.basic)).isChecked();
|
||||||
|
DataContext.AdvancedEnabled = ((CheckBox) findViewById(R.id.advanced)).isChecked();
|
||||||
|
DataContext.ExpertEnabled = ((CheckBox) findViewById(R.id.expert)).isChecked();
|
||||||
|
DataContext.MasterEnabled = ((CheckBox) findViewById(R.id.master)).isChecked();
|
||||||
|
DataContext.RemasterEnabled = ((CheckBox) findViewById(R.id.remaster)).isChecked();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveOptions() {
|
||||||
|
DataContext.CopyUrl = ((Switch) findViewById(R.id.copyUrl)).isChecked();
|
||||||
|
DataContext.AutoLaunch = ((Switch) findViewById(R.id.autoLaunch)).isChecked();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void loadContextData() {
|
||||||
|
String username = mContextSp.getString("username", null);
|
||||||
|
String password = mContextSp.getString("password", null);
|
||||||
|
boolean copyUrl = mContextSp.getBoolean("copyUrl", true);
|
||||||
|
boolean autoLaunch = mContextSp.getBoolean("autoLaunch", true);
|
||||||
|
|
||||||
|
boolean basicEnabled = mContextSp.getBoolean("basicEnabled", false);
|
||||||
|
boolean advancedEnabled = mContextSp.getBoolean("advancedEnabled", false);
|
||||||
|
boolean expertEnabled = mContextSp.getBoolean("expertEnabled", true);
|
||||||
|
boolean masterEnabled = mContextSp.getBoolean("masterEnabled", true);
|
||||||
|
boolean remasterEnabled = mContextSp.getBoolean("remasterEnabled", true);
|
||||||
|
|
||||||
|
String proxyHost = mContextSp.getString("porxyHost","proxy.bakapiano.com");
|
||||||
|
String webHost = mContextSp.getString("webHost","");
|
||||||
|
int proxyPort = mContextSp.getInt("porxyPort",2569);
|
||||||
|
|
||||||
|
SharedPreferences settingProperties = getSharedPreferences("setting", Context.MODE_PRIVATE);
|
||||||
|
|
||||||
|
SharedPreferences.Editor editorSetting = settingProperties.edit();
|
||||||
|
SharedPreferences.Editor editorM = mContextSp.edit();
|
||||||
|
if(settingProperties.contains("shuiyu_username")) {
|
||||||
|
username = settingProperties.getString("shuiyu_username","");
|
||||||
|
editorM.putString("username",username);
|
||||||
|
editorM.apply();
|
||||||
|
}else if(username != null){
|
||||||
|
editorSetting.putString("shuiyu_username",username);
|
||||||
|
editorSetting.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
((TextView) findViewById(R.id.username)).setText(username);
|
||||||
|
((TextView) findViewById(R.id.password)).setText(password);
|
||||||
|
|
||||||
|
((Switch) findViewById(R.id.copyUrl)).setChecked(copyUrl);
|
||||||
|
((Switch) findViewById(R.id.autoLaunch)).setChecked(autoLaunch);
|
||||||
|
|
||||||
|
((CheckBox) findViewById(R.id.basic)).setChecked(basicEnabled);
|
||||||
|
((CheckBox) findViewById(R.id.advanced)).setChecked(advancedEnabled);
|
||||||
|
((CheckBox) findViewById(R.id.expert)).setChecked(expertEnabled);
|
||||||
|
((CheckBox) findViewById(R.id.master)).setChecked(masterEnabled);
|
||||||
|
((CheckBox) findViewById(R.id.remaster)).setChecked(remasterEnabled);
|
||||||
|
|
||||||
|
|
||||||
|
DataContext.Username = username;
|
||||||
|
DataContext.Password = password;
|
||||||
|
|
||||||
|
DataContext.CopyUrl = copyUrl;
|
||||||
|
DataContext.AutoLaunch = autoLaunch;
|
||||||
|
|
||||||
|
DataContext.BasicEnabled = basicEnabled;
|
||||||
|
DataContext.AdvancedEnabled = advancedEnabled;
|
||||||
|
DataContext.ExpertEnabled = expertEnabled;
|
||||||
|
DataContext.MasterEnabled = masterEnabled;
|
||||||
|
DataContext.RemasterEnabled = remasterEnabled;
|
||||||
|
|
||||||
|
DataContext.ProxyPort = proxyPort;
|
||||||
|
DataContext.ProxyHost = proxyHost;
|
||||||
|
DataContext.WebHost = webHost;
|
||||||
|
|
||||||
|
Button button2 = findViewById(R.id.button2);
|
||||||
|
button2.setOnClickListener(v -> {
|
||||||
|
Toast.makeText(this, "正在跳转至水鱼查分器官网", Toast.LENGTH_SHORT).show();
|
||||||
|
String url = "https://www.diving-fish.com/maimaidx/prober/";
|
||||||
|
Uri uri = Uri.parse(url);
|
||||||
|
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
|
||||||
|
startActivity(intent);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveContextData() {
|
||||||
|
SharedPreferences.Editor editor = mContextSp.edit();
|
||||||
|
saveAccountContextData(editor);
|
||||||
|
saveOptionsContextData(editor);
|
||||||
|
saveDifficultiesContextData(editor);
|
||||||
|
editor.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void saveDifficultiesContextData(SharedPreferences.Editor editor) {
|
||||||
|
editor.putBoolean("basicEnabled", DataContext.BasicEnabled);
|
||||||
|
editor.putBoolean("advancedEnabled", DataContext.AdvancedEnabled);
|
||||||
|
editor.putBoolean("expertEnabled", DataContext.ExpertEnabled);
|
||||||
|
editor.putBoolean("masterEnabled", DataContext.MasterEnabled);
|
||||||
|
editor.putBoolean("remasterEnabled", DataContext.RemasterEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void saveOptionsContextData(SharedPreferences.Editor editor) {
|
||||||
|
editor.putBoolean("copyUrl", DataContext.CopyUrl);
|
||||||
|
editor.putBoolean("autoLaunch", DataContext.AutoLaunch);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void saveAccountContextData(SharedPreferences.Editor editor) {
|
||||||
|
editor.putString("username", DataContext.Username);
|
||||||
|
editor.putString("password", DataContext.Password);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getWechatApi() {
|
||||||
|
try {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_MAIN);
|
||||||
|
ComponentName cmp = new ComponentName("com.tencent.mm", "com.tencent.mm.ui.LauncherUI");
|
||||||
|
intent.addCategory(Intent.CATEGORY_LAUNCHER);
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
intent.setComponent(cmp);
|
||||||
|
startActivity(intent);
|
||||||
|
} catch (ActivityNotFoundException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getRandomString(int length) {
|
||||||
|
String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||||
|
Random random = new Random();
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
int number = random.nextInt(62);
|
||||||
|
sb.append(str.charAt(number));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -253,11 +253,12 @@ public class HomeFragment extends Fragment {
|
|||||||
// 设置 Toolbar 标题
|
// 设置 Toolbar 标题
|
||||||
String navHomeLabel = getString(R.string.menu_home);
|
String navHomeLabel = getString(R.string.menu_home);
|
||||||
Toolbar toolbar = ((MainActivity) requireActivity()).findViewById(R.id.toolbar);
|
Toolbar toolbar = ((MainActivity) requireActivity()).findViewById(R.id.toolbar);
|
||||||
toolbar.setTitle("FindMaimaiDX - " + a.size() + " 店铺" + "\n" + tot);
|
if(!(toolbar.getTitle().equals("歌曲成绩") || toolbar.getTitle().equals("地图")|| toolbar.getTitle().equals("设置"))) {
|
||||||
|
toolbar.setTitle("FindMaimaiDX - " + a.size() + " 店铺" + "\n" + tot);
|
||||||
|
}
|
||||||
|
|
||||||
// 更新 SharedViewModel 中的 Map
|
// 更新 SharedViewModel 中的 Map
|
||||||
sharedViewModel.addToMap("places", new Gson().toJson(a));
|
sharedViewModel.setPlacelist(new ArrayList<>(a));
|
||||||
|
|
||||||
// 通知适配器数据已更改
|
// 通知适配器数据已更改
|
||||||
adapter.notifyDataSetChanged();
|
adapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
@@ -306,7 +307,6 @@ public class HomeFragment extends Fragment {
|
|||||||
public void onLocationChanged(@NonNull Location location) {
|
public void onLocationChanged(@NonNull Location location) {
|
||||||
Log.d("Location", "onLocationChanged");
|
Log.d("Location", "onLocationChanged");
|
||||||
if (flag) {
|
if (flag) {
|
||||||
Toast.makeText(context, "定位成功", Toast.LENGTH_SHORT).show();
|
|
||||||
// 调用高德地图 API 进行逆地理编码
|
// 调用高德地图 API 进行逆地理编码
|
||||||
reverseGeocode(location.getLatitude(), location.getLongitude());
|
reverseGeocode(location.getLatitude(), location.getLongitude());
|
||||||
}
|
}
|
||||||
@@ -392,12 +392,17 @@ public class HomeFragment extends Fragment {
|
|||||||
String province = address.getAdminArea();
|
String province = address.getAdminArea();
|
||||||
String city = address.getLocality();
|
String city = address.getLocality();
|
||||||
// 更新 UI
|
// 更新 UI
|
||||||
requireActivity().runOnUiThread(() -> {
|
try {
|
||||||
tot = detail;
|
requireActivity().runOnUiThread(() -> {
|
||||||
this.province = province;
|
tot = detail;
|
||||||
this.city = city;
|
this.province = province;
|
||||||
extracted();
|
this.city = city;
|
||||||
});
|
extracted();
|
||||||
|
});
|
||||||
|
}catch (Exception e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
Log.d("Location", "Android 自带 Geocoder 获取地址失败");
|
Log.d("Location", "Android 自带 Geocoder 获取地址失败");
|
||||||
setDefaultLocation(); // 设置默认位置
|
setDefaultLocation(); // 设置默认位置
|
||||||
|
|||||||
@@ -0,0 +1,319 @@
|
|||||||
|
package org.astral.findmaimaiultra.ui.music;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import com.bumptech.glide.Glide;
|
||||||
|
import com.bumptech.glide.request.RequestOptions;
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import okhttp3.*;
|
||||||
|
import org.astral.findmaimaiultra.R;
|
||||||
|
import org.astral.findmaimaiultra.adapter.MusicRatingAdapter;
|
||||||
|
import org.astral.findmaimaiultra.been.faker.MaiUser;
|
||||||
|
import org.astral.findmaimaiultra.been.faker.MusicRating;
|
||||||
|
import org.astral.findmaimaiultra.been.faker.UserMusicList;
|
||||||
|
import org.astral.findmaimaiultra.databinding.FragmentMusicBinding;
|
||||||
|
import org.astral.findmaimaiultra.ui.MainActivity;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class MusicFragment extends Fragment {
|
||||||
|
private FragmentMusicBinding binding;
|
||||||
|
private SharedPreferences setting;
|
||||||
|
private SharedPreferences scorePrefs;
|
||||||
|
private RecyclerView recyclerView;
|
||||||
|
private MusicRatingAdapter adapter;
|
||||||
|
private List<UserMusicList> musicSongsRatings;
|
||||||
|
private List<MusicRating> musicRatings = new ArrayList<>();
|
||||||
|
private String userId;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
// 获取 SharedPreferences 实例
|
||||||
|
setting = requireContext().getSharedPreferences("setting", Context.MODE_PRIVATE);
|
||||||
|
scorePrefs = requireContext().getSharedPreferences("score", Context.MODE_PRIVATE);
|
||||||
|
userId = setting.getString("userId", "未知");
|
||||||
|
// 读取音乐评分列表
|
||||||
|
musicSongsRatings = loadMusicRatings();
|
||||||
|
int totalMusicRatings = 0;
|
||||||
|
for (UserMusicList musicSongsRating : musicSongsRatings) {
|
||||||
|
musicRatings.addAll(musicSongsRating.getUserMusicDetailList());
|
||||||
|
for (MusicRating musicRating : musicSongsRating.getUserMusicDetailList()) {
|
||||||
|
totalMusicRatings += musicRating.getRating();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 假设这里填充了音乐评分数据
|
||||||
|
if (musicRatings.isEmpty()) {
|
||||||
|
MusicRating empty = new MusicRating();
|
||||||
|
empty.setMusicName("空-请去导入成绩");
|
||||||
|
musicRatings.add(empty);
|
||||||
|
}else {
|
||||||
|
Toolbar toolbar = ((MainActivity) requireActivity()).findViewById(R.id.toolbar);
|
||||||
|
toolbar.setTitle("歌曲成绩 - 总共" + musicRatings.size() + "首");
|
||||||
|
Toast.makeText(getContext(), "总共" + musicRatings.size() + "首,有效rating:" + totalMusicRatings, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveMusicRatings(List<UserMusicList> musicRatings) {
|
||||||
|
Gson gson = new Gson();
|
||||||
|
String json = gson.toJson(musicRatings);
|
||||||
|
SharedPreferences.Editor editor = scorePrefs.edit();
|
||||||
|
editor.putString("musicRatings", json);
|
||||||
|
editor.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<UserMusicList> loadMusicRatings() {
|
||||||
|
Gson gson = new Gson();
|
||||||
|
String json = scorePrefs.getString("musicRatings", null);
|
||||||
|
if (json == null) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
Type type = new TypeToken<List<UserMusicList>>() {}.getType();
|
||||||
|
return gson.fromJson(json, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||||
|
ViewGroup container, Bundle savedInstanceState) {
|
||||||
|
MusicViewModel musicViewModel =
|
||||||
|
new ViewModelProvider(this).get(MusicViewModel.class);
|
||||||
|
|
||||||
|
binding = FragmentMusicBinding.inflate(inflater, container, false);
|
||||||
|
View root = binding.getRoot();
|
||||||
|
|
||||||
|
recyclerView = binding.getRoot().findViewById(R.id.recyclerView);
|
||||||
|
recyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2)); // 一行显示两个
|
||||||
|
adapter = new MusicRatingAdapter(musicRatings);
|
||||||
|
adapter.setOnItemClickListener(musicRating -> {
|
||||||
|
showMusicDetailDialog(musicRating);
|
||||||
|
});
|
||||||
|
recyclerView.setAdapter(adapter);
|
||||||
|
FloatingActionButton f = binding.fab;
|
||||||
|
f.setOnClickListener(view -> {
|
||||||
|
showOptionsDialog();
|
||||||
|
});
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateScores() {
|
||||||
|
OkHttpClient client = new OkHttpClient();
|
||||||
|
String url = "http://mai.godserver.cn:11451/api/qq/getAAALLL?qq=" + userId;
|
||||||
|
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json"), "");
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.post(requestBody)
|
||||||
|
.build();
|
||||||
|
client.newCall(request).enqueue(new okhttp3.Callback() {
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
String json = response.body().string();
|
||||||
|
MaiUser maiUser = new Gson().fromJson(json, MaiUser.class);
|
||||||
|
saveMusicRatings(maiUser.getUserMusicList());
|
||||||
|
requireActivity().runOnUiThread(() -> {
|
||||||
|
musicRatings.clear();
|
||||||
|
for (UserMusicList musicSongsRating : maiUser.getUserMusicList()) {
|
||||||
|
musicRatings.addAll(musicSongsRating.getUserMusicDetailList());
|
||||||
|
}
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NotNull Call call, @NotNull IOException e) {
|
||||||
|
// 处理失败情况
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showMusicDetailDialog(MusicRating musicRating) {
|
||||||
|
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext(), R.style.CustomDialogStyle);
|
||||||
|
View dialogView = LayoutInflater.from(requireContext()).inflate(R.layout.music_dialog, null);
|
||||||
|
builder.setView(dialogView);
|
||||||
|
|
||||||
|
ImageView musicImageView = dialogView.findViewById(R.id.dialog_music_image);
|
||||||
|
TextView musicNameTextView = dialogView.findViewById(R.id.dialog_music_name);
|
||||||
|
TextView musicAchievementTextView = dialogView.findViewById(R.id.dialog_music_achievement);
|
||||||
|
TextView musicRatingTextView = dialogView.findViewById(R.id.dialog_music_rating);
|
||||||
|
TextView musicLevelInfoTextView = dialogView.findViewById(R.id.dialog_music_level_info);
|
||||||
|
ImageView musicTypeImageView = dialogView.findViewById(R.id.dialog_music_type);
|
||||||
|
ImageView musicComboStatusTextView = dialogView.findViewById(R.id.dialog_music_combo_status);
|
||||||
|
TextView musicPlayCountTextView = dialogView.findViewById(R.id.dialog_music_play_count);
|
||||||
|
|
||||||
|
// 设置图像曲绘
|
||||||
|
int id = musicRating.getMusicId();
|
||||||
|
if (id > 10000) {
|
||||||
|
id = id - 10000;
|
||||||
|
}
|
||||||
|
String imageUrl = "https://assets2.lxns.net/maimai/jacket/" + id + ".png";
|
||||||
|
Glide.with(this)
|
||||||
|
.load(imageUrl)
|
||||||
|
.into(musicImageView);
|
||||||
|
|
||||||
|
// 设置详细数据
|
||||||
|
musicNameTextView.setText(musicRating.getMusicName());
|
||||||
|
String ac = String.valueOf(musicRating.getAchievement());
|
||||||
|
if (ac.length() > 4) {
|
||||||
|
ac = ac.substring(0, ac.length() - 4) + "." + ac.substring(ac.length() - 4);
|
||||||
|
}
|
||||||
|
musicAchievementTextView.setText("达成率: " + ac);
|
||||||
|
musicRatingTextView.setText("Rating " + String.valueOf(musicRating.getRating()));
|
||||||
|
musicLevelInfoTextView.setText("Level " + String.valueOf(musicRating.getLevel_info()));
|
||||||
|
|
||||||
|
// 设置 musicTypeImageView 的图片并等比例缩小到 75dp
|
||||||
|
int targetWidth = (int) (75 * getResources().getDisplayMetrics().density); // 75dp 转换为像素
|
||||||
|
RequestOptions requestOptions = new RequestOptions()
|
||||||
|
.override(targetWidth, targetWidth) // 设置宽度和高度为 75dp 对应的像素值
|
||||||
|
.centerInside(); // 确保图片等比例缩放
|
||||||
|
|
||||||
|
if (musicRating.getType().equals("dx")) {
|
||||||
|
Glide.with(this)
|
||||||
|
.load(R.drawable.dx)
|
||||||
|
.apply(requestOptions)
|
||||||
|
.into(musicTypeImageView);
|
||||||
|
} else {
|
||||||
|
Glide.with(this)
|
||||||
|
.load(R.drawable.sd)
|
||||||
|
.apply(requestOptions)
|
||||||
|
.into(musicTypeImageView);
|
||||||
|
}
|
||||||
|
|
||||||
|
int comboType = musicRating.getComboStatus();
|
||||||
|
if (comboType == 1) {
|
||||||
|
Glide.with(this)
|
||||||
|
.load(R.drawable.fc)
|
||||||
|
.apply(requestOptions)
|
||||||
|
.into(musicComboStatusTextView);
|
||||||
|
} else if (comboType == 2) {
|
||||||
|
Glide.with(this)
|
||||||
|
.load(R.drawable.fcp)
|
||||||
|
.apply(requestOptions)
|
||||||
|
.into(musicComboStatusTextView);
|
||||||
|
} else if (comboType == 3) {
|
||||||
|
Glide.with(this)
|
||||||
|
.load(R.drawable.ap)
|
||||||
|
.apply(requestOptions)
|
||||||
|
.into(musicComboStatusTextView);
|
||||||
|
} else if (comboType == 4) {
|
||||||
|
Glide.with(this)
|
||||||
|
.load(R.drawable.app)
|
||||||
|
.apply(requestOptions)
|
||||||
|
.into(musicComboStatusTextView);
|
||||||
|
}
|
||||||
|
|
||||||
|
musicPlayCountTextView.setText("PC: "+ String.valueOf(musicRating.getPlayCount()));
|
||||||
|
|
||||||
|
builder.setPositiveButton("确定", (dialog, which) -> {
|
||||||
|
// 点击确定按钮后的操作
|
||||||
|
// 例如:关闭对话框
|
||||||
|
dialog.dismiss();
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.setNegativeButton("取消", (dialog, which) -> {
|
||||||
|
// 点击取消按钮后的操作
|
||||||
|
// 例如:关闭对话框
|
||||||
|
dialog.dismiss();
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showOptionsDialog() {
|
||||||
|
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext(), R.style.CustomDialogStyle);
|
||||||
|
builder.setTitle("选项");
|
||||||
|
builder.setItems(new CharSequence[]{"更新数据", "分数排序", "搜索指定歌曲"}, (dialog, which) -> {
|
||||||
|
switch (which) {
|
||||||
|
case 0:
|
||||||
|
// 更新数据
|
||||||
|
if (userId.equals("未知")) {
|
||||||
|
new MaterialAlertDialogBuilder(requireContext(), R.style.CustomDialogStyle)
|
||||||
|
.setMessage("请先绑定机器人")
|
||||||
|
.setPositiveButton("确定", (d, w) -> d.dismiss())
|
||||||
|
.show();
|
||||||
|
} else {
|
||||||
|
new MaterialAlertDialogBuilder(requireContext(), R.style.CustomDialogStyle)
|
||||||
|
.setMessage("是否更新?")
|
||||||
|
.setPositiveButton("确定", (d, w) -> {
|
||||||
|
updateScores();
|
||||||
|
d.dismiss();
|
||||||
|
})
|
||||||
|
.setNegativeButton("cancel", (d, w) -> d.dismiss())
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
// 分数排序
|
||||||
|
sortMusicRatingsByRating();
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
// 搜索指定歌曲
|
||||||
|
showSearchDialog();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
builder.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sortMusicRatingsByRating() {
|
||||||
|
Collections.sort(musicRatings, new Comparator<MusicRating>() {
|
||||||
|
@Override
|
||||||
|
public int compare(MusicRating o1, MusicRating o2) {
|
||||||
|
return Integer.compare(o2.getRating(), o1.getRating()); // 降序排序
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showSearchDialog() {
|
||||||
|
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext(), R.style.CustomDialogStyle);
|
||||||
|
View dialogView = LayoutInflater.from(requireContext()).inflate(R.layout.search_dialog, null);
|
||||||
|
builder.setView(dialogView);
|
||||||
|
|
||||||
|
TextView searchInput = dialogView.findViewById(R.id.search_input);
|
||||||
|
builder.setPositiveButton("搜索", (dialog, which) -> {
|
||||||
|
String query = searchInput.getText().toString().trim();
|
||||||
|
searchMusicRatings(query);
|
||||||
|
});
|
||||||
|
builder.setNegativeButton("取消", (dialog, which) -> dialog.dismiss());
|
||||||
|
|
||||||
|
builder.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void searchMusicRatings(String query) {
|
||||||
|
List<MusicRating> filteredList = new ArrayList<>();
|
||||||
|
for (MusicRating musicRating : musicRatings) {
|
||||||
|
if (musicRating.getMusicName().toLowerCase().contains(query.toLowerCase())) {
|
||||||
|
filteredList.add(musicRating);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
musicRatings.clear();
|
||||||
|
musicRatings.addAll(filteredList);
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package org.astral.findmaimaiultra.ui.music;
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
import androidx.lifecycle.ViewModel;
|
||||||
|
|
||||||
|
public class MusicViewModel extends ViewModel {
|
||||||
|
|
||||||
|
private final MutableLiveData<String> mText;
|
||||||
|
|
||||||
|
public MusicViewModel() {
|
||||||
|
mText = new MutableLiveData<>();
|
||||||
|
mText.setValue("This is slideshow fragment");
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<String> getText() {
|
||||||
|
return mText;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater;
|
||||||
|
|
||||||
|
import android.content.ClipData;
|
||||||
|
import android.content.ClipboardManager;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.ui.DataContext;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static androidx.core.content.ContextCompat.getSystemService;
|
||||||
|
|
||||||
|
public class Util {
|
||||||
|
public static Set<Integer> getDifficulties() {
|
||||||
|
Set<Integer> set = new HashSet<>();
|
||||||
|
if (DataContext.BasicEnabled)
|
||||||
|
set.add(0);
|
||||||
|
if (DataContext.AdvancedEnabled)
|
||||||
|
set.add(1);
|
||||||
|
if (DataContext.ExpertEnabled)
|
||||||
|
set.add(2);
|
||||||
|
if (DataContext.MasterEnabled)
|
||||||
|
set.add(3);
|
||||||
|
if (DataContext.RemasterEnabled)
|
||||||
|
set.add(4);
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void copyText(Context context, String link) {
|
||||||
|
ClipboardManager clipboard = Objects.requireNonNull(getSystemService(context, ClipboardManager.class));
|
||||||
|
ClipData clip = ClipData.newPlainText("link", link);
|
||||||
|
clipboard.setPrimaryClip(clip);
|
||||||
|
Toast.makeText(context, "已复制链接,请在微信中粘贴并打开", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.crawler;
|
||||||
|
|
||||||
|
public interface Callback {
|
||||||
|
void onResponse(Object result);
|
||||||
|
|
||||||
|
default void onError(Exception error) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.crawler;
|
||||||
|
|
||||||
|
import android.os.Handler;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.ui.DataContext;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.core.LocalVpnService;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
|
||||||
|
import static org.astral.findmaimaiultra.utill.updater.Util.getDifficulties;
|
||||||
|
|
||||||
|
|
||||||
|
public class CrawlerCaller {
|
||||||
|
private static final String TAG = "CrawlerCaller";
|
||||||
|
private static final Handler m_Handler = new Handler();
|
||||||
|
public static LocalVpnService.onStatusChangedListener listener;
|
||||||
|
|
||||||
|
static public String getWechatAuthUrl() {
|
||||||
|
try {
|
||||||
|
WechatCrawler crawler = new WechatCrawler();
|
||||||
|
String url = crawler.getWechatAuthUrl();
|
||||||
|
return url;
|
||||||
|
} catch (IOException error) {
|
||||||
|
writeLog("获取微信登录url时出现错误:");
|
||||||
|
writeLog(error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static public void writeLog(String text) {
|
||||||
|
m_Handler.post(() -> listener.onLogReceived(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
static public void writeLog(Exception e) {
|
||||||
|
StringWriter sw = new StringWriter();
|
||||||
|
e.printStackTrace(new PrintWriter(sw));
|
||||||
|
String exceptionAsString = sw.toString();
|
||||||
|
m_Handler.post(() -> listener.onLogReceived(exceptionAsString));
|
||||||
|
}
|
||||||
|
|
||||||
|
static public void fetchData(String authUrl) {
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
Thread.sleep(3000);
|
||||||
|
LocalVpnService.IsRunning = false;
|
||||||
|
Thread.sleep(3000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
writeLog(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
WechatCrawler crawler = new WechatCrawler();
|
||||||
|
crawler.fetchAndUploadData(DataContext.Username, DataContext.Password, getDifficulties(), authUrl);
|
||||||
|
} catch (IOException e) {
|
||||||
|
writeLog(e);
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
static public void verifyAccount(String username, String password, Callback callback) {
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
WechatCrawler crawler = new WechatCrawler();
|
||||||
|
Boolean result = crawler.verifyProberAccount(username, password);
|
||||||
|
callback.onResponse(result);
|
||||||
|
} catch (IOException error) {
|
||||||
|
error.printStackTrace();
|
||||||
|
callback.onError(error);
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
static public void getLatestVersion(Callback callback)
|
||||||
|
{
|
||||||
|
new Thread(() -> {
|
||||||
|
WechatCrawler crawler = new WechatCrawler();
|
||||||
|
String result = crawler.getLatestVersion();
|
||||||
|
callback.onResponse(result);
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.crawler;
|
||||||
|
|
||||||
|
import okhttp3.Cookie;
|
||||||
|
import okhttp3.CookieJar;
|
||||||
|
import okhttp3.HttpUrl;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class SimpleCookieJar implements CookieJar {
|
||||||
|
private final Map<String, List<Cookie>> cookieStore = new HashMap<String, List<Cookie>>();
|
||||||
|
private final Object lock = new Object();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveFromResponse(HttpUrl httpUrl, List<Cookie> newCookies) {
|
||||||
|
synchronized (lock) {
|
||||||
|
HashMap<String, Cookie> map = new HashMap<>();
|
||||||
|
List<Cookie> oldCookies = cookieStore.get(httpUrl.host());
|
||||||
|
if (oldCookies != null) {
|
||||||
|
for (Cookie cookie : oldCookies) {
|
||||||
|
map.put(cookie.name(), cookie);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Override old cookie with same name
|
||||||
|
if (newCookies != null) {
|
||||||
|
for (Cookie cookie : newCookies) {
|
||||||
|
map.put(cookie.name(), cookie);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<Cookie> mergedList = new ArrayList<Cookie>();
|
||||||
|
for (Map.Entry<String, Cookie> pair : map.entrySet()) {
|
||||||
|
mergedList.add(pair.getValue());
|
||||||
|
}
|
||||||
|
cookieStore.put(httpUrl.host(), mergedList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Cookie> loadForRequest(HttpUrl httpUrl) {
|
||||||
|
synchronized (lock) {
|
||||||
|
List<Cookie> cookies = cookieStore.get(httpUrl.host());
|
||||||
|
return cookies != null ? cookies : new ArrayList<Cookie>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearCookieStroe() {
|
||||||
|
synchronized (lock) {
|
||||||
|
this.cookieStore.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.crawler;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
import okhttp3.*;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.notification.NotificationUtil;
|
||||||
|
|
||||||
|
import javax.net.ssl.*;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import static org.astral.findmaimaiultra.utill.updater.crawler.CrawlerCaller.writeLog;
|
||||||
|
|
||||||
|
|
||||||
|
public class WechatCrawler {
|
||||||
|
// Make this true for Fiddler to capture https request
|
||||||
|
private static final boolean IGNORE_CERT = false;
|
||||||
|
|
||||||
|
private static final int MAX_RETRY_COUNT = 4;
|
||||||
|
|
||||||
|
private static final String TAG = "Crawler";
|
||||||
|
|
||||||
|
private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
|
||||||
|
|
||||||
|
private static final MediaType TEXT = MediaType.parse("text/plain");
|
||||||
|
|
||||||
|
private static final SimpleCookieJar jar = new SimpleCookieJar();
|
||||||
|
|
||||||
|
private static final Map<Integer, String> diffMap = new HashMap<>();
|
||||||
|
|
||||||
|
private static OkHttpClient client = null;
|
||||||
|
|
||||||
|
public WechatCrawler() {
|
||||||
|
diffMap.put(0, "Basic");
|
||||||
|
diffMap.put(1, "Advance");
|
||||||
|
diffMap.put(2, "Expert");
|
||||||
|
diffMap.put(3, "Master");
|
||||||
|
diffMap.put(4, "Re:Master");
|
||||||
|
buildHttpClient(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void uploadData(Integer diff, String data, Integer retryCount) {
|
||||||
|
Request request = new Request.Builder().url("https://www.diving-fish.com/api/pageparser/page").addHeader("content-type", "text/plain").post(RequestBody.create(data, TEXT)).build();
|
||||||
|
//Log.d("Crawler", "Uploading data to server" + "\n" + data);
|
||||||
|
Call call = client.newCall((request));
|
||||||
|
|
||||||
|
try {
|
||||||
|
Response response = call.execute();
|
||||||
|
String result = response.body().string();
|
||||||
|
writeLog(diffMap.get(diff) + " 难度数据上传完成:" + result);
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
retryUploadData(e, diff, data, retryCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void retryUploadData(Exception e, Integer diff, String data, Integer currentRetryCount) {
|
||||||
|
writeLog("上传 " + diffMap.get(diff) + " 分数数据至水鱼查分器时出现错误: " + e);
|
||||||
|
if (currentRetryCount < MAX_RETRY_COUNT) {
|
||||||
|
writeLog("进行第" + currentRetryCount.toString() + "次重试");
|
||||||
|
uploadData(diff, data, currentRetryCount + 1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
writeLog(diffMap.get(diff) + "难度数据上传失败!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void fetchAndUploadData(String username, String password, Set<Integer> difficulties) {
|
||||||
|
List<CompletableFuture<Object>> tasks = new ArrayList<>();
|
||||||
|
for (Integer diff : difficulties) {
|
||||||
|
tasks.add(CompletableFuture.supplyAsync(() -> {
|
||||||
|
fetchAndUploadData(username, password, diff, 1);
|
||||||
|
return null;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
for (CompletableFuture<Object> task: tasks) {
|
||||||
|
task.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void fetchAndUploadData(String username, String password, Integer diff, Integer retryCount) {
|
||||||
|
writeLog("开始获取 " + diffMap.get(diff) + " 难度的数据");
|
||||||
|
Request request = new Request.Builder().url("https://maimai.wahlap.com/maimai-mobile/record/musicGenre/search/?genre=99&diff=" + diff).build();
|
||||||
|
|
||||||
|
Call call = client.newCall(request);
|
||||||
|
try {
|
||||||
|
Response response = call.execute();
|
||||||
|
String data = Objects.requireNonNull(response.body()).string();
|
||||||
|
Matcher matcher = Pattern.compile("<html.*>([\\s\\S]*)</html>").matcher(data);
|
||||||
|
if (matcher.find()) data = Objects.requireNonNull(matcher.group(1));
|
||||||
|
data = Pattern.compile("\\s+").matcher(data).replaceAll(" ");
|
||||||
|
|
||||||
|
// Upload data to maimai-prober
|
||||||
|
writeLog(diffMap.get(diff) + " 难度的数据已获取,正在上传至水鱼查分器");
|
||||||
|
uploadData(diff, "<login><u>" + username + "</u><p>" + password + "</p></login>" + data, 1);
|
||||||
|
} catch (Exception e) {
|
||||||
|
retryFetchAndUploadData(e, username, password, diff, retryCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void retryFetchAndUploadData(Exception e, String username, String password, Integer diff, Integer currentRetryCount) {
|
||||||
|
writeLog("获取 " + diffMap.get(diff) + " 难度数据时出现错误: " + e);
|
||||||
|
if (currentRetryCount < MAX_RETRY_COUNT) {
|
||||||
|
writeLog("进行第" + currentRetryCount.toString() + "次重试");
|
||||||
|
fetchAndUploadData(username, password, diff, currentRetryCount + 1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
writeLog(diffMap.get(diff) + "难度数据更新失败!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean verifyProberAccount(String username, String password) throws IOException {
|
||||||
|
String data = String.format("{\"username\" : \"%s\", \"password\" : \"%s\"}", username, password);
|
||||||
|
RequestBody body = RequestBody.create(JSON, data);
|
||||||
|
|
||||||
|
Request request = new Request.Builder().addHeader("Host", "www.diving-fish.com").addHeader("Origin", "https://www.diving-fish.com").addHeader("Referer", "https://www.diving-fish.com/maimaidx/prober/").url("https://www.diving-fish.com/api/maimaidxprober/login").post(body).build();
|
||||||
|
|
||||||
|
Call call = client.newCall(request);
|
||||||
|
Response response = call.execute();
|
||||||
|
String responseBody = response.body().string();
|
||||||
|
|
||||||
|
Log.d(TAG, "Verify account: " + responseBody + response);
|
||||||
|
return !responseBody.contains("errcode");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getWechatAuthUrl() throws IOException {
|
||||||
|
this.buildHttpClient(true);
|
||||||
|
|
||||||
|
Request request = new Request.Builder().addHeader("Host", "tgk-wcaime.wahlap.com").addHeader("Upgrade-Insecure-Requests", "1").addHeader("User-Agent", "Mozilla/5.0 (Linux; Android 12; IN2010 Build/RKQ1.211119.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/86.0.4240.99 XWEB/4317 MMWEBSDK/20220903 Mobile Safari/537.36 MMWEBID/363 MicroMessenger/8.0.28.2240(0x28001C57) WeChat/arm64 Weixin NetType/WIFI Language/zh_CN ABI/arm64").addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/wxpic,image/tpg,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9").addHeader("X-Requested-With", "com.tencent.mm").addHeader("Sec-Fetch-Site", "none").addHeader("Sec-Fetch-Mode", "navigate").addHeader("Sec-Fetch-User", "?1").addHeader("Sec-Fetch-Dest", "document").addHeader("Accept-Encoding", "gzip, deflate").addHeader("Accept-Language", "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7").url("https://tgk-wcaime.wahlap.com/wc_auth/oauth/authorize/maimai-dx").build();
|
||||||
|
|
||||||
|
Call call = client.newCall(request);
|
||||||
|
Response response = call.execute();
|
||||||
|
String url = response.request().url().toString().replace("redirect_uri=https", "redirect_uri=http");
|
||||||
|
|
||||||
|
Log.d(TAG, "Auth url:" + url);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void fetchAndUploadData(String username, String password, Set<Integer> difficulties, String wechatAuthUrl) throws IOException {
|
||||||
|
if (wechatAuthUrl.startsWith("http"))
|
||||||
|
wechatAuthUrl = wechatAuthUrl.replaceFirst("http", "https");
|
||||||
|
|
||||||
|
jar.clearCookieStroe();
|
||||||
|
|
||||||
|
// Login wechat
|
||||||
|
try {
|
||||||
|
writeLog("开始登录net,请稍后...");
|
||||||
|
this.loginWechat(wechatAuthUrl);
|
||||||
|
writeLog("登陆完成");
|
||||||
|
} catch (Exception error) {
|
||||||
|
writeLog("登陆时出现错误:\n");
|
||||||
|
writeLog(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch maimai data
|
||||||
|
try {
|
||||||
|
this.fetchMaimaiData(username, password, difficulties);
|
||||||
|
writeLog("maimai 数据更新完成");
|
||||||
|
} catch (Exception error) {
|
||||||
|
writeLog("maimai 数据更新时出现错误:");
|
||||||
|
writeLog(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Fetch chuithm data
|
||||||
|
// this.fetchChunithmData(username, password);
|
||||||
|
NotificationUtil.getINSTANCE().stopNotification();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getLatestVersion() {
|
||||||
|
this.buildHttpClient(true);
|
||||||
|
|
||||||
|
Request request = new Request.Builder().get().url("https://maimaidx-prober-updater-android.bakapiano.com/version").build();
|
||||||
|
|
||||||
|
Call call = client.newCall(request);
|
||||||
|
try {
|
||||||
|
Response response = call.execute();
|
||||||
|
return response.body().string().trim();
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loginWechat(String wechatAuthUrl) throws Exception {
|
||||||
|
this.buildHttpClient(true);
|
||||||
|
|
||||||
|
Log.d(TAG, wechatAuthUrl);
|
||||||
|
|
||||||
|
Request request = new Request.Builder().addHeader("Host", "tgk-wcaime.wahlap.com").addHeader("Upgrade-Insecure-Requests", "1").addHeader("User-Agent", "Mozilla/5.0 (Linux; Android 12; IN2010 Build/RKQ1.211119.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/86.0.4240.99 XWEB/4317 MMWEBSDK/20220903 Mobile Safari/537.36 MMWEBID/363 MicroMessenger/8.0.28.2240(0x28001C57) WeChat/arm64 Weixin NetType/WIFI Language/zh_CN ABI/arm64").addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/wxpic,image/tpg,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9").addHeader("X-Requested-With", "com.tencent.mm").addHeader("Sec-Fetch-Site", "none").addHeader("Sec-Fetch-Mode", "navigate").addHeader("Sec-Fetch-User", "?1").addHeader("Sec-Fetch-Dest", "document").addHeader("Accept-Encoding", "gzip, deflate").addHeader("Accept-Language", "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7").get().url(wechatAuthUrl).build();
|
||||||
|
|
||||||
|
Call call = client.newCall(request);
|
||||||
|
Response response = call.execute();
|
||||||
|
|
||||||
|
try {
|
||||||
|
String responseBody = response.body().string();
|
||||||
|
Log.d(TAG, responseBody);
|
||||||
|
} catch (NullPointerException error) {
|
||||||
|
writeLog(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
int code = response.code();
|
||||||
|
writeLog(String.valueOf(code));
|
||||||
|
if (code >= 400) {
|
||||||
|
throw new Exception("登陆时出现错误,请重试!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle redirect manually
|
||||||
|
String location = response.headers().get("Location");
|
||||||
|
if (response.code() >= 300 && response.code() < 400 && location != null) {
|
||||||
|
request = new Request.Builder().url(location).get().build();
|
||||||
|
call = client.newCall(request);
|
||||||
|
call.execute().close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fetchMaimaiData(String username, String password, Set<Integer> difficulties) throws IOException {
|
||||||
|
this.buildHttpClient(false);
|
||||||
|
fetchAndUploadData(username, password, difficulties);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fetchChunithmData(String username, String password) throws IOException {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildHttpClient(boolean followRedirect) {
|
||||||
|
OkHttpClient.Builder builder = new OkHttpClient.Builder();
|
||||||
|
|
||||||
|
if (IGNORE_CERT) ignoreCertBuilder(builder);
|
||||||
|
|
||||||
|
builder.connectTimeout(120, TimeUnit.SECONDS);
|
||||||
|
builder.readTimeout(120, TimeUnit.SECONDS);
|
||||||
|
builder.writeTimeout(120, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
builder.followRedirects(followRedirect);
|
||||||
|
builder.followSslRedirects(followRedirect);
|
||||||
|
|
||||||
|
builder.cookieJar(jar);
|
||||||
|
|
||||||
|
// No cache for http request
|
||||||
|
builder.cache(null);
|
||||||
|
Interceptor noCacheInterceptor = chain -> {
|
||||||
|
Request request = chain.request();
|
||||||
|
Request.Builder builder1 = request.newBuilder().addHeader("Cache-Control", "no-cache");
|
||||||
|
request = builder1.build();
|
||||||
|
return chain.proceed(request);
|
||||||
|
};
|
||||||
|
builder.addInterceptor(noCacheInterceptor);
|
||||||
|
|
||||||
|
// Fix SSL handle shake error
|
||||||
|
ConnectionSpec spec = new ConnectionSpec.Builder(ConnectionSpec.COMPATIBLE_TLS).tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_1, TlsVersion.TLS_1_0).allEnabledCipherSuites().build();
|
||||||
|
// 兼容http接口
|
||||||
|
ConnectionSpec spec1 = new ConnectionSpec.Builder(ConnectionSpec.CLEARTEXT).build();
|
||||||
|
builder.connectionSpecs(Arrays.asList(spec, spec1));
|
||||||
|
|
||||||
|
builder.pingInterval(3, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
client = builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ignoreCertBuilder(OkHttpClient.Builder builder) {
|
||||||
|
try {
|
||||||
|
final TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
|
||||||
|
@Override
|
||||||
|
public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
|
||||||
|
return new java.security.cert.X509Certificate[]{};
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
final SSLContext sslContext = SSLContext.getInstance("SSL");
|
||||||
|
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
|
||||||
|
// Create an ssl socket factory with our all-trusting manager
|
||||||
|
final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
|
||||||
|
builder.sslSocketFactory(sslSocketFactory, (X509TrustManager) trustAllCerts[0]);
|
||||||
|
builder.hostnameVerifier(new HostnameVerifier() {
|
||||||
|
@Override
|
||||||
|
public boolean verify(String hostname, SSLSession session) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.notification;
|
||||||
|
|
||||||
|
import android.app.*;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.graphics.BitmapFactory;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.util.Log;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import org.astral.findmaimaiultra.R;
|
||||||
|
import org.astral.findmaimaiultra.ui.UpdateActivity;
|
||||||
|
|
||||||
|
|
||||||
|
public class NotificationUtil extends Service {
|
||||||
|
|
||||||
|
private volatile static NotificationUtil INSTANCE;
|
||||||
|
private Context mContext;
|
||||||
|
private static final String TAG = "notification";
|
||||||
|
|
||||||
|
public static NotificationUtil getINSTANCE() {
|
||||||
|
if (INSTANCE == null) {
|
||||||
|
synchronized (NotificationUtil.class) {
|
||||||
|
if (INSTANCE == null) {
|
||||||
|
INSTANCE = new NotificationUtil();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public NotificationUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public NotificationUtil setContext(Context mContext) {
|
||||||
|
Log.d(TAG, "setContext: " + mContext);
|
||||||
|
this.mContext = mContext;
|
||||||
|
return getINSTANCE();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startNotification() {
|
||||||
|
Log.d(TAG, "startNotification: " + mContext);
|
||||||
|
Intent notificationIntent = new Intent(mContext, this.getClass());
|
||||||
|
mContext.startService(notificationIntent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopNotification() {
|
||||||
|
Intent notificationIntent = new Intent(mContext, this.getClass());
|
||||||
|
mContext.stopService(notificationIntent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
createNotificationChannel();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
stopForeground(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||||
|
return START_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createNotificationChannel() {
|
||||||
|
Notification.Builder builder = new Notification.Builder(getApplicationContext());
|
||||||
|
Intent nfIntent = new Intent(this, UpdateActivity.class);
|
||||||
|
nfIntent.setAction(Intent.ACTION_MAIN);
|
||||||
|
nfIntent.addCategory(Intent.CATEGORY_LAUNCHER);
|
||||||
|
nfIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
|
||||||
|
|
||||||
|
int pendingIntentFlags = PendingIntent.FLAG_UPDATE_CURRENT;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
pendingIntentFlags |= PendingIntent.FLAG_IMMUTABLE; // 或者使用 PendingIntent.FLAG_MUTABLE
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.setContentIntent(PendingIntent.getActivity(this, 0, nfIntent, pendingIntentFlags))
|
||||||
|
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
|
||||||
|
.setContentTitle("更新器后台运行通知")
|
||||||
|
.setSmallIcon(R.mipmap.ic_launcher)
|
||||||
|
.setContentText("正在努力的帮你上传分数")
|
||||||
|
.setWhen(System.currentTimeMillis());
|
||||||
|
|
||||||
|
/*以下是对Android 8.0的适配*/
|
||||||
|
//普通notification适配
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
builder.setChannelId("updater_keep_alive_channel");
|
||||||
|
NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||||
|
NotificationChannel channel = new NotificationChannel("updater_keep_alive_channel", "更新器后台运行通知", NotificationManager.IMPORTANCE_LOW);
|
||||||
|
notificationManager.createNotificationChannel(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
Notification notification = builder.build();
|
||||||
|
notification.defaults = Notification.DEFAULT_SOUND;
|
||||||
|
startForeground(12001, notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.server;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
import fi.iki.elonen.NanoHTTPD;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class HttpRedirectServer extends NanoHTTPD {
|
||||||
|
public static int Port = 9457;
|
||||||
|
private final static String TAG = "HttpRedirectServer";
|
||||||
|
|
||||||
|
protected HttpRedirectServer() throws IOException {
|
||||||
|
super(Port);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() throws IOException {
|
||||||
|
super.start();
|
||||||
|
Log.d(TAG, "Http server running on http://localhost:" + Port);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response serve(IHTTPSession session) {
|
||||||
|
return newFixedLengthResponse(
|
||||||
|
Response.Status.ACCEPTED,
|
||||||
|
MIME_HTML,
|
||||||
|
"<html><body><h1>登录信息已获取,可关闭该窗口并请切回到更新器等待分数上传!</h1></body></html><script>alert('登录信息已获取,请切回到更新器等待分数上传!');</script>");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.server;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
import fi.iki.elonen.NanoHTTPD;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.crawler.CrawlerCaller;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import static org.astral.findmaimaiultra.utill.updater.ui.DataContext.HookHost;
|
||||||
|
|
||||||
|
|
||||||
|
public class HttpServer extends NanoHTTPD {
|
||||||
|
public static int Port = 8284;
|
||||||
|
private final static String TAG = "HttpServer";
|
||||||
|
|
||||||
|
protected HttpServer() throws IOException {
|
||||||
|
super(Port);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() throws IOException {
|
||||||
|
super.start();
|
||||||
|
Log.d(TAG, "Http server running on http://localhost:" + Port);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response serve(IHTTPSession session) {
|
||||||
|
Log.d(TAG, "Serve request: " + session.getUri());
|
||||||
|
switch (session.getUri()) {
|
||||||
|
case "/auth":
|
||||||
|
return redirectToWechatAuthUrl(session);
|
||||||
|
default:
|
||||||
|
return redirectToAuthUrlWithRandomParm(session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// To avoid fu***ing cache of wechat webview client
|
||||||
|
private Response redirectToAuthUrlWithRandomParm(IHTTPSession session) {
|
||||||
|
Response r = newFixedLengthResponse(Response.Status.REDIRECT, MIME_HTML, "");
|
||||||
|
r.addHeader("Location", "http://" + HookHost + "/auth?random=" + System.currentTimeMillis());
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Response redirectToWechatAuthUrl(IHTTPSession session) {
|
||||||
|
String url = CrawlerCaller.getWechatAuthUrl();
|
||||||
|
if (url == null)
|
||||||
|
return newFixedLengthResponse(Response.Status.BAD_REQUEST, MIME_HTML, "");
|
||||||
|
Log.d(TAG, url);
|
||||||
|
|
||||||
|
Response r = newFixedLengthResponse(Response.Status.REDIRECT, MIME_HTML, "");
|
||||||
|
r.addHeader("Location", url);
|
||||||
|
r.addHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||||
|
r.addHeader("Pragma", "no-cache");
|
||||||
|
r.addHeader("Expires", "0");
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.server;
|
||||||
|
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.util.Log;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class HttpServerService extends Service {
|
||||||
|
private static final String TAG = "HttpServerService";
|
||||||
|
private HttpServer httpServer;
|
||||||
|
private HttpRedirectServer httpRedirectServer;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
Log.d("HttpService", "Http service on create");
|
||||||
|
try {
|
||||||
|
if (this.httpServer != null) this.httpServer.stop();
|
||||||
|
if (this.httpRedirectServer != null) this.httpRedirectServer.stop();
|
||||||
|
this.httpServer = new HttpServer();
|
||||||
|
this.httpRedirectServer = new HttpRedirectServer();
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.d(TAG, "Error while create HttpServerService: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||||
|
super.onStartCommand(intent, flags, startId);
|
||||||
|
Log.d("HttpService", "Http service on start command");
|
||||||
|
try {
|
||||||
|
this.httpServer.start();
|
||||||
|
this.httpRedirectServer.start();
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.d(TAG, "Error while start HttpServerService: " + e);
|
||||||
|
}
|
||||||
|
return START_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
this.httpServer.stop();
|
||||||
|
this.httpRedirectServer.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.ui;
|
||||||
|
|
||||||
|
public class DataContext {
|
||||||
|
public static String Username = null;
|
||||||
|
|
||||||
|
public static String Password = null;
|
||||||
|
|
||||||
|
public static boolean CopyUrl = true;
|
||||||
|
|
||||||
|
public static boolean AutoLaunch = true;
|
||||||
|
|
||||||
|
public static String HookHost = "127.0.0.1:8284";
|
||||||
|
|
||||||
|
public static String WebHost = "";
|
||||||
|
|
||||||
|
public static String ProxyHost = "proxy.bakapiano.com";
|
||||||
|
|
||||||
|
public static int ProxyPort = 2569;
|
||||||
|
|
||||||
|
public static boolean CompatibleMode = false;
|
||||||
|
|
||||||
|
public static boolean BasicEnabled = false;
|
||||||
|
|
||||||
|
public static boolean AdvancedEnabled = false;
|
||||||
|
|
||||||
|
public static boolean ExpertEnabled = true;
|
||||||
|
|
||||||
|
public static boolean MasterEnabled = true;
|
||||||
|
|
||||||
|
public static boolean RemasterEnabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.vpn.core;
|
||||||
|
|
||||||
|
public class Constant {
|
||||||
|
public static final String TAG = "VpnProxy";
|
||||||
|
}
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.vpn.core;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
import android.util.SparseArray;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.dns.DnsPacket;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.dns.Question;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.dns.Resource;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.dns.ResourcePointer;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.tcpip.CommonMethods;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.tcpip.IPHeader;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.tcpip.UDPHeader;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.DatagramPacket;
|
||||||
|
import java.net.DatagramSocket;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
|
||||||
|
public class DnsProxy implements Runnable {
|
||||||
|
|
||||||
|
private static final ConcurrentHashMap<Integer, String> IPDomainMaps = new ConcurrentHashMap<Integer, String>();
|
||||||
|
private static final ConcurrentHashMap<String, Integer> DomainIPMaps = new ConcurrentHashMap<String, Integer>();
|
||||||
|
private final long QUERY_TIMEOUT_NS = 10 * 1000000000L;
|
||||||
|
public boolean Stopped;
|
||||||
|
private DatagramSocket m_Client;
|
||||||
|
private Thread m_ReceivedThread;
|
||||||
|
private short m_QueryID;
|
||||||
|
private SparseArray<QueryState> m_QueryArray;
|
||||||
|
|
||||||
|
public DnsProxy() throws IOException {
|
||||||
|
m_QueryArray = new SparseArray<QueryState>();
|
||||||
|
m_Client = new DatagramSocket(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String reverseLookup(int ip) {
|
||||||
|
return IPDomainMaps.get(ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void start() {
|
||||||
|
m_ReceivedThread = new Thread(this);
|
||||||
|
m_ReceivedThread.setName("DnsProxyThread");
|
||||||
|
m_ReceivedThread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void stop() {
|
||||||
|
Stopped = true;
|
||||||
|
if (m_Client != null) {
|
||||||
|
try {
|
||||||
|
m_Client.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(Constant.TAG, "Exception when closing m_Client", e);
|
||||||
|
} finally {
|
||||||
|
m_Client = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
byte[] RECEIVE_BUFFER = new byte[2000];
|
||||||
|
IPHeader ipHeader = new IPHeader(RECEIVE_BUFFER, 0);
|
||||||
|
ipHeader.Default();
|
||||||
|
UDPHeader udpHeader = new UDPHeader(RECEIVE_BUFFER, 20);
|
||||||
|
|
||||||
|
ByteBuffer dnsBuffer = ByteBuffer.wrap(RECEIVE_BUFFER);
|
||||||
|
dnsBuffer.position(28);
|
||||||
|
dnsBuffer = dnsBuffer.slice();
|
||||||
|
|
||||||
|
DatagramPacket packet = new DatagramPacket(RECEIVE_BUFFER, 28, RECEIVE_BUFFER.length - 28);
|
||||||
|
|
||||||
|
while (m_Client != null && !m_Client.isClosed()) {
|
||||||
|
|
||||||
|
packet.setLength(RECEIVE_BUFFER.length - 28);
|
||||||
|
m_Client.receive(packet);
|
||||||
|
|
||||||
|
dnsBuffer.clear();
|
||||||
|
dnsBuffer.limit(packet.getLength());
|
||||||
|
try {
|
||||||
|
DnsPacket dnsPacket = DnsPacket.FromBytes(dnsBuffer);
|
||||||
|
if (dnsPacket != null) {
|
||||||
|
OnDnsResponseReceived(ipHeader, udpHeader, dnsPacket);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(Constant.TAG, "Exception when reading DNS packet", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(Constant.TAG, "Exception in DnsResolver main loop", e);
|
||||||
|
} finally {
|
||||||
|
Log.d(Constant.TAG, "DnsResolver Thread Exited.");
|
||||||
|
this.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getFirstIP(DnsPacket dnsPacket) {
|
||||||
|
for (int i = 0; i < dnsPacket.Header.ResourceCount; i++) {
|
||||||
|
Resource resource = dnsPacket.Resources[i];
|
||||||
|
if (resource.Type == 1) {
|
||||||
|
int ip = CommonMethods.readInt(resource.Data, 0);
|
||||||
|
return ip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tamperDnsResponse(byte[] rawPacket, DnsPacket dnsPacket, int newIP) {
|
||||||
|
Question question = dnsPacket.Questions[0];
|
||||||
|
|
||||||
|
dnsPacket.Header.setResourceCount((short) 1);
|
||||||
|
dnsPacket.Header.setAResourceCount((short) 0);
|
||||||
|
dnsPacket.Header.setEResourceCount((short) 0);
|
||||||
|
|
||||||
|
ResourcePointer rPointer = new ResourcePointer(rawPacket, question.Offset() + question.Length());
|
||||||
|
rPointer.setDomain((short) 0xC00C);
|
||||||
|
rPointer.setType(question.Type);
|
||||||
|
rPointer.setClass(question.Class);
|
||||||
|
rPointer.setTTL(ProxyConfig.Instance.getDnsTTL());
|
||||||
|
rPointer.setDataLength((short) 4);
|
||||||
|
rPointer.setIP(newIP);
|
||||||
|
|
||||||
|
dnsPacket.Size = 12 + question.Length() + 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getOrCreateFakeIP(String domainString) {
|
||||||
|
Integer fakeIP = DomainIPMaps.get(domainString);
|
||||||
|
if (fakeIP == null) {
|
||||||
|
int hashIP = domainString.hashCode();
|
||||||
|
do {
|
||||||
|
fakeIP = ProxyConfig.FAKE_NETWORK_IP | (hashIP & 0x0000FFFF);
|
||||||
|
hashIP++;
|
||||||
|
} while (IPDomainMaps.containsKey(fakeIP));
|
||||||
|
|
||||||
|
DomainIPMaps.put(domainString, fakeIP);
|
||||||
|
IPDomainMaps.put(fakeIP, domainString);
|
||||||
|
}
|
||||||
|
return fakeIP;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDnsResponseReceived(IPHeader ipHeader, UDPHeader udpHeader, DnsPacket dnsPacket) {
|
||||||
|
QueryState state = null;
|
||||||
|
synchronized (m_QueryArray) {
|
||||||
|
state = m_QueryArray.get(dnsPacket.Header.ID);
|
||||||
|
if (state != null) {
|
||||||
|
m_QueryArray.remove(dnsPacket.Header.ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state != null) {
|
||||||
|
dnsPacket.Header.setID(state.ClientQueryID);
|
||||||
|
ipHeader.setSourceIP(state.RemoteIP);
|
||||||
|
ipHeader.setDestinationIP(state.ClientIP);
|
||||||
|
ipHeader.setProtocol(IPHeader.UDP);
|
||||||
|
ipHeader.setTotalLength(20 + 8 + dnsPacket.Size);
|
||||||
|
udpHeader.setSourcePort(state.RemotePort);
|
||||||
|
udpHeader.setDestinationPort(state.ClientPort);
|
||||||
|
udpHeader.setTotalLength(8 + dnsPacket.Size);
|
||||||
|
|
||||||
|
LocalVpnService.Instance.sendUDPPacket(ipHeader, udpHeader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getIPFromCache(String domain) {
|
||||||
|
Integer ip = DomainIPMaps.get(domain);
|
||||||
|
if (ip == null) {
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
return ip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean interceptDns(IPHeader ipHeader, UDPHeader udpHeader, DnsPacket dnsPacket) {
|
||||||
|
Question question = dnsPacket.Questions[0];
|
||||||
|
|
||||||
|
if (ProxyConfig.IS_DEBUG)
|
||||||
|
Log.d(Constant.TAG, "DNS Query " + question.Domain);
|
||||||
|
|
||||||
|
if (question.Type == 1) {
|
||||||
|
if (ProxyConfig.Instance.needProxy(question.Domain)) {
|
||||||
|
int fakeIP = getOrCreateFakeIP(question.Domain);
|
||||||
|
tamperDnsResponse(ipHeader.m_Data, dnsPacket, fakeIP);
|
||||||
|
|
||||||
|
if (ProxyConfig.IS_DEBUG)
|
||||||
|
Log.d(Constant.TAG, "interceptDns FakeDns: " +
|
||||||
|
question.Domain + " " +
|
||||||
|
CommonMethods.ipIntToString(fakeIP));
|
||||||
|
|
||||||
|
int sourceIP = ipHeader.getSourceIP();
|
||||||
|
short sourcePort = udpHeader.getSourcePort();
|
||||||
|
ipHeader.setSourceIP(ipHeader.getDestinationIP());
|
||||||
|
ipHeader.setDestinationIP(sourceIP);
|
||||||
|
ipHeader.setTotalLength(20 + 8 + dnsPacket.Size);
|
||||||
|
udpHeader.setSourcePort(udpHeader.getDestinationPort());
|
||||||
|
udpHeader.setDestinationPort(sourcePort);
|
||||||
|
udpHeader.setTotalLength(8 + dnsPacket.Size);
|
||||||
|
LocalVpnService.Instance.sendUDPPacket(ipHeader, udpHeader);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearExpiredQueries() {
|
||||||
|
long now = System.nanoTime();
|
||||||
|
for (int i = m_QueryArray.size() - 1; i >= 0; i--) {
|
||||||
|
QueryState state = m_QueryArray.valueAt(i);
|
||||||
|
if ((now - state.QueryNanoTime) > QUERY_TIMEOUT_NS) {
|
||||||
|
m_QueryArray.removeAt(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onDnsRequestReceived(IPHeader ipHeader, UDPHeader udpHeader, DnsPacket dnsPacket) {
|
||||||
|
if (!interceptDns(ipHeader, udpHeader, dnsPacket)) {
|
||||||
|
QueryState state = new QueryState();
|
||||||
|
state.ClientQueryID = dnsPacket.Header.ID;
|
||||||
|
state.QueryNanoTime = System.nanoTime();
|
||||||
|
state.ClientIP = ipHeader.getSourceIP();
|
||||||
|
state.ClientPort = udpHeader.getSourcePort();
|
||||||
|
state.RemoteIP = ipHeader.getDestinationIP();
|
||||||
|
state.RemotePort = udpHeader.getDestinationPort();
|
||||||
|
|
||||||
|
m_QueryID++;
|
||||||
|
dnsPacket.Header.setID(m_QueryID);
|
||||||
|
|
||||||
|
synchronized (m_QueryArray) {
|
||||||
|
clearExpiredQueries();
|
||||||
|
m_QueryArray.put(m_QueryID, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
InetSocketAddress remoteAddress = new InetSocketAddress(CommonMethods.ipIntToInet4Address(state.RemoteIP), state.RemotePort);
|
||||||
|
DatagramPacket packet = new DatagramPacket(udpHeader.m_Data, udpHeader.m_Offset + 8, dnsPacket.Size);
|
||||||
|
packet.setSocketAddress(remoteAddress);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (LocalVpnService.Instance.protect(m_Client)) {
|
||||||
|
m_Client.send(packet);
|
||||||
|
} else {
|
||||||
|
Log.e(Constant.TAG, "VPN protect udp socket failed.");
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(Constant.TAG, "protect", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class QueryState {
|
||||||
|
public short ClientQueryID;
|
||||||
|
public long QueryNanoTime;
|
||||||
|
public int ClientIP;
|
||||||
|
public short ClientPort;
|
||||||
|
public int RemoteIP;
|
||||||
|
public short RemotePort;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.vpn.core;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.tcpip.CommonMethods;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
|
||||||
|
public class HttpHostHeaderParser {
|
||||||
|
|
||||||
|
public static String parseHost(byte[] buffer, int offset, int count) {
|
||||||
|
try {
|
||||||
|
switch (buffer[offset]) {
|
||||||
|
case 'G'://GET
|
||||||
|
case 'H'://HEAD
|
||||||
|
case 'P'://POST,PUT
|
||||||
|
case 'D'://DELETE
|
||||||
|
case 'O'://OPTIONS
|
||||||
|
case 'T'://TRACE
|
||||||
|
case 'C'://CONNECT
|
||||||
|
return getHttpHost(buffer, offset, count);
|
||||||
|
case 0x16://SSL
|
||||||
|
return getSNI(buffer, offset, count);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
// LocalVpnService.Instance.writeLog("Error: parseHost:%s", e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getHttpHost(byte[] buffer, int offset, int count) {
|
||||||
|
String headerString = new String(buffer, offset, count);
|
||||||
|
String[] headerLines = headerString.split("\\r\\n");
|
||||||
|
String requestLine = headerLines[0];
|
||||||
|
if (requestLine.startsWith("GET") || requestLine.startsWith("POST") || requestLine.startsWith("HEAD") || requestLine.startsWith("OPTIONS")) {
|
||||||
|
for (int i = 1; i < headerLines.length; i++) {
|
||||||
|
String[] nameValueStrings = headerLines[i].split(":");
|
||||||
|
if (nameValueStrings.length == 2) {
|
||||||
|
String name = nameValueStrings[0].toLowerCase(Locale.ENGLISH).trim();
|
||||||
|
String value = nameValueStrings[1].trim();
|
||||||
|
if ("host".equals(name)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getSNI(byte[] buffer, int offset, int count) {
|
||||||
|
int limit = offset + count;
|
||||||
|
if (count > 43 && buffer[offset] == 0x16) {//TLS Client Hello
|
||||||
|
offset += 43;//skip 43 bytes header
|
||||||
|
|
||||||
|
//read sessionID:
|
||||||
|
if (offset + 1 > limit) return null;
|
||||||
|
int sessionIDLength = buffer[offset++] & 0xFF;
|
||||||
|
offset += sessionIDLength;
|
||||||
|
|
||||||
|
//read cipher suites:
|
||||||
|
if (offset + 2 > limit) return null;
|
||||||
|
int cipherSuitesLength = CommonMethods.readShort(buffer, offset) & 0xFFFF;
|
||||||
|
offset += 2;
|
||||||
|
offset += cipherSuitesLength;
|
||||||
|
|
||||||
|
//read Compression method:
|
||||||
|
if (offset + 1 > limit) return null;
|
||||||
|
int compressionMethodLength = buffer[offset++] & 0xFF;
|
||||||
|
offset += compressionMethodLength;
|
||||||
|
|
||||||
|
if (offset == limit) {
|
||||||
|
System.err.println("TLS Client Hello packet doesn't contains SNI info.(offset == limit)");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
//read Extensions:
|
||||||
|
if (offset + 2 > limit) return null;
|
||||||
|
int extensionsLength = CommonMethods.readShort(buffer, offset) & 0xFFFF;
|
||||||
|
offset += 2;
|
||||||
|
|
||||||
|
if (offset + extensionsLength > limit) {
|
||||||
|
System.err.println("TLS Client Hello packet is incomplete.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (offset + 4 <= limit) {
|
||||||
|
int type0 = buffer[offset++] & 0xFF;
|
||||||
|
int type1 = buffer[offset++] & 0xFF;
|
||||||
|
int length = CommonMethods.readShort(buffer, offset) & 0xFFFF;
|
||||||
|
offset += 2;
|
||||||
|
|
||||||
|
if (type0 == 0x00 && type1 == 0x00 && length > 5) { //have SNI
|
||||||
|
offset += 5;//skip SNI header.
|
||||||
|
length -= 5;//SNI size;
|
||||||
|
if (offset + length > limit) return null;
|
||||||
|
String serverName = new String(buffer, offset, length);
|
||||||
|
if (ProxyConfig.IS_DEBUG)
|
||||||
|
Log.d(Constant.TAG, "SNI: " + serverName);
|
||||||
|
return serverName;
|
||||||
|
} else {
|
||||||
|
offset += length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
System.err.println("TLS Client Hello packet doesn't contains Host field info.");
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
System.err.println("Bad TLS Client Hello packet.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,423 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.vpn.core;
|
||||||
|
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.SharedPreferences.Editor;
|
||||||
|
import android.content.pm.PackageInfo;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.net.VpnService;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
|
import android.util.Log;
|
||||||
|
import org.astral.findmaimaiultra.R;
|
||||||
|
import org.astral.findmaimaiultra.ui.UpdateActivity;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.dns.DnsPacket;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.tcpip.CommonMethods;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.tcpip.IPHeader;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.tcpip.TCPHeader;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.tcpip.UDPHeader;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
import static android.service.controls.ControlsProviderService.TAG;
|
||||||
|
|
||||||
|
|
||||||
|
public class LocalVpnService extends VpnService implements Runnable {
|
||||||
|
|
||||||
|
public static LocalVpnService Instance;
|
||||||
|
public static boolean IsRunning = false;
|
||||||
|
|
||||||
|
private final String device = Build.DEVICE;
|
||||||
|
private final String model = Build.MODEL;
|
||||||
|
private final String version = "" + Build.VERSION.SDK_INT + " (" + Build.VERSION.RELEASE + ")";
|
||||||
|
|
||||||
|
private static int ID;
|
||||||
|
private static int LOCAL_IP;
|
||||||
|
private static ConcurrentHashMap<onStatusChangedListener, Object> m_OnStatusChangedListeners = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private Thread m_VPNThread;
|
||||||
|
private ParcelFileDescriptor m_VPNInterface;
|
||||||
|
private TcpProxyServer m_TcpProxyServer;
|
||||||
|
private DnsProxy m_DnsProxy;
|
||||||
|
private FileOutputStream m_VPNOutputStream;
|
||||||
|
|
||||||
|
private byte[] m_Packet;
|
||||||
|
private IPHeader m_IPHeader;
|
||||||
|
private TCPHeader m_TCPHeader;
|
||||||
|
private UDPHeader m_UDPHeader;
|
||||||
|
private ByteBuffer m_DNSBuffer;
|
||||||
|
private Handler m_Handler;
|
||||||
|
private long m_SentBytes;
|
||||||
|
private long m_ReceivedBytes;
|
||||||
|
private String[] m_Blacklist;
|
||||||
|
|
||||||
|
public LocalVpnService() {
|
||||||
|
ID++;
|
||||||
|
m_Handler = new Handler();
|
||||||
|
m_Packet = new byte[20000];
|
||||||
|
m_IPHeader = new IPHeader(m_Packet, 0);
|
||||||
|
m_TCPHeader = new TCPHeader(m_Packet, 20);
|
||||||
|
m_UDPHeader = new UDPHeader(m_Packet, 20);
|
||||||
|
m_DNSBuffer = ((ByteBuffer) ByteBuffer.wrap(m_Packet).position(28)).slice();
|
||||||
|
Instance = this;
|
||||||
|
|
||||||
|
Log.d("VpnProxy", "New VPNService" + ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void addOnStatusChangedListener(onStatusChangedListener listener) {
|
||||||
|
if (!m_OnStatusChangedListeners.containsKey(listener)) {
|
||||||
|
m_OnStatusChangedListeners.put(listener, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void removeOnStatusChangedListener(onStatusChangedListener listener) {
|
||||||
|
if (m_OnStatusChangedListeners.containsKey(listener)) {
|
||||||
|
m_OnStatusChangedListeners.remove(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
try {
|
||||||
|
m_TcpProxyServer = new TcpProxyServer(0);
|
||||||
|
m_TcpProxyServer.start();
|
||||||
|
writeLog("LocalTcpServer started.");
|
||||||
|
|
||||||
|
m_DnsProxy = new DnsProxy();
|
||||||
|
m_DnsProxy.start();
|
||||||
|
} catch (Exception e) {
|
||||||
|
writeLog("Failed to start TCP/DNS Proxy");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||||
|
super.onStartCommand(intent, flags, startId);
|
||||||
|
IsRunning = true;
|
||||||
|
// Start a new session by creating a new thread.
|
||||||
|
m_VPNThread = new Thread(this, "VPNServiceThread");
|
||||||
|
m_VPNThread.start();
|
||||||
|
return START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
String action = intent.getAction();
|
||||||
|
if (action.equals(VpnService.SERVICE_INTERFACE)) {
|
||||||
|
return super.onBind(intent);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onStatusChanged(final String status, final boolean isRunning) {
|
||||||
|
m_Handler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
for (Map.Entry<onStatusChangedListener, Object> entry : m_OnStatusChangedListeners.entrySet()) {
|
||||||
|
entry.getKey().onStatusChanged(status, isRunning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeLog(final String format, Object... args) {
|
||||||
|
final String logString = String.format(format, args);
|
||||||
|
m_Handler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
for (Map.Entry<onStatusChangedListener, Object> entry : m_OnStatusChangedListeners.entrySet()) {
|
||||||
|
entry.getKey().onLogReceived(logString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendUDPPacket(IPHeader ipHeader, UDPHeader udpHeader) {
|
||||||
|
try {
|
||||||
|
CommonMethods.ComputeUDPChecksum(ipHeader, udpHeader);
|
||||||
|
this.m_VPNOutputStream.write(ipHeader.m_Data, ipHeader.m_Offset, ipHeader.getTotalLength());
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String getAppInstallID() {
|
||||||
|
SharedPreferences preferences = getSharedPreferences(TAG, MODE_PRIVATE);
|
||||||
|
String appInstallID = preferences.getString("AppInstallID", null);
|
||||||
|
if (appInstallID == null || appInstallID.isEmpty()) {
|
||||||
|
appInstallID = UUID.randomUUID().toString();
|
||||||
|
Editor editor = preferences.edit();
|
||||||
|
editor.putString("AppInstallID", appInstallID);
|
||||||
|
editor.commit();
|
||||||
|
}
|
||||||
|
return appInstallID;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getVersionName() {
|
||||||
|
try {
|
||||||
|
PackageManager packageManager = getPackageManager();
|
||||||
|
PackageInfo packInfo = packageManager.getPackageInfo(getPackageName(), 0);
|
||||||
|
String version = packInfo.versionName;
|
||||||
|
return version;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "0.0";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String configDirFor(Context context, String suffix) {
|
||||||
|
return new File(context.getFilesDir().getAbsolutePath(), ".lantern" + suffix).getAbsolutePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void run() {
|
||||||
|
try {
|
||||||
|
Log.d(TAG, "VPNService work thread is running... " + ID);
|
||||||
|
|
||||||
|
ProxyConfig.AppInstallID = getAppInstallID();
|
||||||
|
ProxyConfig.AppVersion = getVersionName();
|
||||||
|
writeLog("Android version: %s", Build.VERSION.RELEASE);
|
||||||
|
writeLog("App version: %s", ProxyConfig.AppVersion);
|
||||||
|
|
||||||
|
waitUntilPrepared();
|
||||||
|
|
||||||
|
runVPN();
|
||||||
|
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Log.e(TAG, "Exception", e);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
writeLog("Fatal error: %s", e.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
writeLog("VpnProxy terminated.");
|
||||||
|
dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runVPN() throws Exception {
|
||||||
|
this.m_VPNInterface = establishVPN();
|
||||||
|
this.m_VPNOutputStream = new FileOutputStream(m_VPNInterface.getFileDescriptor());
|
||||||
|
FileInputStream in = new FileInputStream(m_VPNInterface.getFileDescriptor());
|
||||||
|
try {
|
||||||
|
while (IsRunning) {
|
||||||
|
boolean idle = true;
|
||||||
|
int size = in.read(m_Packet);
|
||||||
|
if (size > 0) {
|
||||||
|
if (m_DnsProxy.Stopped || m_TcpProxyServer.Stopped) {
|
||||||
|
in.close();
|
||||||
|
throw new Exception("LocalServer stopped.");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
onIPPacketReceived(m_IPHeader, size);
|
||||||
|
idle = false;
|
||||||
|
} catch (IOException ex) {
|
||||||
|
Log.e(TAG, "IOException when processing IP packet", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (idle) {
|
||||||
|
Thread.sleep(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
in.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onIPPacketReceived(IPHeader ipHeader, int size) throws IOException {
|
||||||
|
switch (ipHeader.getProtocol()) {
|
||||||
|
case IPHeader.TCP:
|
||||||
|
TCPHeader tcpHeader = m_TCPHeader;
|
||||||
|
tcpHeader.m_Offset = ipHeader.getHeaderLength();
|
||||||
|
if (ipHeader.getSourceIP() == LOCAL_IP) {
|
||||||
|
if (tcpHeader.getSourcePort() == m_TcpProxyServer.Port) {
|
||||||
|
NatSession session = NatSessionManager.getSession(tcpHeader.getDestinationPort());
|
||||||
|
if (session != null) {
|
||||||
|
ipHeader.setSourceIP(ipHeader.getDestinationIP());
|
||||||
|
tcpHeader.setSourcePort(session.RemotePort);
|
||||||
|
ipHeader.setDestinationIP(LOCAL_IP);
|
||||||
|
|
||||||
|
CommonMethods.ComputeTCPChecksum(ipHeader, tcpHeader);
|
||||||
|
m_VPNOutputStream.write(ipHeader.m_Data, ipHeader.m_Offset, size);
|
||||||
|
m_ReceivedBytes += size;
|
||||||
|
} else {
|
||||||
|
if (ProxyConfig.IS_DEBUG)
|
||||||
|
Log.d(TAG, "NoSession: " +
|
||||||
|
ipHeader.toString() + " " +
|
||||||
|
tcpHeader.toString());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
int portKey = tcpHeader.getSourcePort();
|
||||||
|
NatSession session = NatSessionManager.getSession(portKey);
|
||||||
|
if (session == null || session.RemoteIP != ipHeader.getDestinationIP() || session.RemotePort != tcpHeader.getDestinationPort()) {
|
||||||
|
session = NatSessionManager.createSession(portKey, ipHeader.getDestinationIP(), tcpHeader.getDestinationPort());
|
||||||
|
}
|
||||||
|
|
||||||
|
session.LastNanoTime = System.nanoTime();
|
||||||
|
session.PacketSent++;
|
||||||
|
|
||||||
|
int tcpDataSize = ipHeader.getDataLength() - tcpHeader.getHeaderLength();
|
||||||
|
if (session.PacketSent == 2 && tcpDataSize == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.BytesSent == 0 && tcpDataSize > 10) {
|
||||||
|
int dataOffset = tcpHeader.m_Offset + tcpHeader.getHeaderLength();
|
||||||
|
String host = HttpHostHeaderParser.parseHost(tcpHeader.m_Data, dataOffset, tcpDataSize);
|
||||||
|
if (host != null) {
|
||||||
|
session.RemoteHost = host;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ipHeader.setSourceIP(ipHeader.getDestinationIP());
|
||||||
|
ipHeader.setDestinationIP(LOCAL_IP);
|
||||||
|
tcpHeader.setDestinationPort(m_TcpProxyServer.Port);
|
||||||
|
|
||||||
|
CommonMethods.ComputeTCPChecksum(ipHeader, tcpHeader);
|
||||||
|
m_VPNOutputStream.write(ipHeader.m_Data, ipHeader.m_Offset, size);
|
||||||
|
session.BytesSent += tcpDataSize;
|
||||||
|
m_SentBytes += size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case IPHeader.UDP:
|
||||||
|
UDPHeader udpHeader = m_UDPHeader;
|
||||||
|
udpHeader.m_Offset = ipHeader.getHeaderLength();
|
||||||
|
if (ipHeader.getSourceIP() == LOCAL_IP && udpHeader.getDestinationPort() == 53) {
|
||||||
|
m_DNSBuffer.clear();
|
||||||
|
m_DNSBuffer.limit(ipHeader.getDataLength() - 8);
|
||||||
|
DnsPacket dnsPacket = DnsPacket.FromBytes(m_DNSBuffer);
|
||||||
|
if (dnsPacket != null && dnsPacket.Header.QuestionCount > 0) {
|
||||||
|
m_DnsProxy.onDnsRequestReceived(ipHeader, udpHeader, dnsPacket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void waitUntilPrepared() {
|
||||||
|
while (prepare(this) != null) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(100);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ParcelFileDescriptor establishVPN() throws Exception {
|
||||||
|
|
||||||
|
NatSessionManager.clearAllSessions();
|
||||||
|
|
||||||
|
Builder builder = new Builder();
|
||||||
|
builder.setMtu(ProxyConfig.Instance.getMTU());
|
||||||
|
|
||||||
|
ProxyConfig.IPAddress ipAddress = ProxyConfig.Instance.getDefaultLocalIP();
|
||||||
|
LOCAL_IP = CommonMethods.ipStringToInt(ipAddress.Address);
|
||||||
|
|
||||||
|
builder.addAddress(ipAddress.Address, ipAddress.PrefixLength);
|
||||||
|
if (ProxyConfig.IS_DEBUG)
|
||||||
|
Log.d(TAG, String.format("addAddress: %s/%d\n", ipAddress.Address, ipAddress.PrefixLength));
|
||||||
|
|
||||||
|
if (m_Blacklist == null) {
|
||||||
|
m_Blacklist = getResources().getStringArray(R.array.black_list);
|
||||||
|
}
|
||||||
|
|
||||||
|
ProxyConfig.Instance.resetDomain(m_Blacklist);
|
||||||
|
|
||||||
|
for (String routeAddress : getResources().getStringArray(R.array.bypass_private_route)) {
|
||||||
|
String[] addr = routeAddress.split("/");
|
||||||
|
builder.addRoute(addr[0], Integer.parseInt(addr[1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.addRoute(CommonMethods.ipIntToString(ProxyConfig.FAKE_NETWORK_IP), 16);
|
||||||
|
|
||||||
|
Intent intent = new Intent(this, UpdateActivity.class);
|
||||||
|
int pendingIntentFlags = PendingIntent.FLAG_UPDATE_CURRENT;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
pendingIntentFlags |= PendingIntent.FLAG_IMMUTABLE; // 或者使用 PendingIntent.FLAG_MUTABLE
|
||||||
|
}
|
||||||
|
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, pendingIntentFlags);
|
||||||
|
builder.setConfigureIntent(pendingIntent);
|
||||||
|
|
||||||
|
builder.setSession(ProxyConfig.Instance.getSessionName());
|
||||||
|
|
||||||
|
ParcelFileDescriptor pfdDescriptor = builder.establish();
|
||||||
|
onStatusChanged(ProxyConfig.Instance.getSessionName() + " " + getString(R.string.vpn_connected_status), true);
|
||||||
|
return pfdDescriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void dispose() {
|
||||||
|
|
||||||
|
onStatusChanged(ProxyConfig.Instance.getSessionName() + " " + getString(R.string.vpn_disconnected_status), false);
|
||||||
|
|
||||||
|
IsRunning = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (m_VPNInterface != null) {
|
||||||
|
m_VPNInterface.close();
|
||||||
|
m_VPNInterface = null;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (m_VPNOutputStream != null) {
|
||||||
|
m_VPNOutputStream.close();
|
||||||
|
m_VPNOutputStream = null;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_VPNThread != null) {
|
||||||
|
m_VPNThread.interrupt();
|
||||||
|
m_VPNThread = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
Log.d(TAG, "VPNService(%s) destroyed: " + ID);
|
||||||
|
if (IsRunning) dispose();
|
||||||
|
try {
|
||||||
|
// ֹͣTcpServer
|
||||||
|
if (m_TcpProxyServer != null) {
|
||||||
|
m_TcpProxyServer.stop();
|
||||||
|
m_TcpProxyServer = null;
|
||||||
|
// writeLog("LocalTcpServer stopped.");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// DnsProxy
|
||||||
|
if (m_DnsProxy != null) {
|
||||||
|
m_DnsProxy.stop();
|
||||||
|
m_DnsProxy = null;
|
||||||
|
// writeLog("LocalDnsProxy stopped.");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface onStatusChangedListener {
|
||||||
|
void onStatusChanged(String status, Boolean isRunning);
|
||||||
|
void onLogReceived(String logString);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.vpn.core;
|
||||||
|
|
||||||
|
public class NatSession {
|
||||||
|
public int RemoteIP;
|
||||||
|
public short RemotePort;
|
||||||
|
public String RemoteHost;
|
||||||
|
public int BytesSent;
|
||||||
|
public int PacketSent;
|
||||||
|
public long LastNanoTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.vpn.core;
|
||||||
|
|
||||||
|
import android.util.SparseArray;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.tcpip.CommonMethods;
|
||||||
|
|
||||||
|
|
||||||
|
public class NatSessionManager {
|
||||||
|
|
||||||
|
static final int MAX_SESSION_COUNT = 4096;
|
||||||
|
static final long SESSION_TIMEOUT_NS = 120 * 1000000000L;
|
||||||
|
static final SparseArray<NatSession> Sessions = new SparseArray<NatSession>();
|
||||||
|
|
||||||
|
public static NatSession getSession(int portKey) {
|
||||||
|
return Sessions.get(portKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getSessionCount() {
|
||||||
|
return Sessions.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void clearExpiredSessions() {
|
||||||
|
long now = System.nanoTime();
|
||||||
|
for (int i = Sessions.size() - 1; i >= 0; i--) {
|
||||||
|
NatSession session = Sessions.valueAt(i);
|
||||||
|
if (now - session.LastNanoTime > SESSION_TIMEOUT_NS) {
|
||||||
|
Sessions.removeAt(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clearAllSessions() {
|
||||||
|
Sessions.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NatSession createSession(int portKey, int remoteIP, short remotePort) {
|
||||||
|
if (Sessions.size() > MAX_SESSION_COUNT) {
|
||||||
|
clearExpiredSessions();
|
||||||
|
}
|
||||||
|
|
||||||
|
NatSession session = new NatSession();
|
||||||
|
session.LastNanoTime = System.nanoTime();
|
||||||
|
session.RemoteIP = remoteIP;
|
||||||
|
session.RemotePort = remotePort;
|
||||||
|
|
||||||
|
// if (ProxyConfig.isFakeIP(remoteIP)) {
|
||||||
|
// session.RemoteHost = DnsProxy.reverseLookup(remoteIP);
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (session.RemoteHost == null) {
|
||||||
|
session.RemoteHost = CommonMethods.ipIntToString(remoteIP);
|
||||||
|
}
|
||||||
|
|
||||||
|
Sessions.put(portKey, session);
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.vpn.core;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
|
import com.otaliastudios.zoom.BuildConfig;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.tcpip.CommonMethods;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.tunnel.Config;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.tunnel.httpconnect.HttpConnectConfig;
|
||||||
|
|
||||||
|
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
|
||||||
|
public class ProxyConfig {
|
||||||
|
public static final ProxyConfig Instance = new ProxyConfig();
|
||||||
|
public final static boolean IS_DEBUG = BuildConfig.DEBUG;
|
||||||
|
public final static int FAKE_NETWORK_MASK = CommonMethods.ipStringToInt("255.255.0.0");
|
||||||
|
public final static int FAKE_NETWORK_IP = CommonMethods.ipStringToInt("26.25.0.0");
|
||||||
|
public static String AppInstallID;
|
||||||
|
public static String AppVersion;
|
||||||
|
|
||||||
|
ArrayList<IPAddress> m_IpList;
|
||||||
|
ArrayList<IPAddress> m_DnsList;
|
||||||
|
ArrayList<Config> m_ProxyList;
|
||||||
|
HashMap<String, Boolean> m_DomainMap;
|
||||||
|
|
||||||
|
int m_dns_ttl = 10;
|
||||||
|
String m_welcome_info = Constant.TAG;
|
||||||
|
String m_session_name = Constant.TAG;
|
||||||
|
String m_user_agent = System.getProperty("http.agent");
|
||||||
|
int m_mtu = 1500;
|
||||||
|
|
||||||
|
|
||||||
|
public ProxyConfig() {
|
||||||
|
m_IpList = new ArrayList<IPAddress>();
|
||||||
|
m_DnsList = new ArrayList<IPAddress>();
|
||||||
|
m_ProxyList = new ArrayList<Config>();
|
||||||
|
m_DomainMap = new HashMap<String, Boolean>();
|
||||||
|
|
||||||
|
m_IpList.add(new IPAddress("26.26.26.2", 32));
|
||||||
|
m_DnsList.add(new IPAddress("119.29.29.29"));
|
||||||
|
m_DnsList.add(new IPAddress("223.5.5.5"));
|
||||||
|
m_DnsList.add(new IPAddress("8.8.8.8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("AuthLeak")
|
||||||
|
public static String getHttpProxyServer(Context ctx) {
|
||||||
|
return ctx.getSharedPreferences("proxyConfig", Context.MODE_PRIVATE)
|
||||||
|
.getString("serverAddress", "http://user1:pass1@192.168.2.10:1082");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setHttpProxyServer(Context ctx, String address) {
|
||||||
|
ctx.getSharedPreferences("proxyConfig", Context.MODE_PRIVATE).edit()
|
||||||
|
.putString("serverAddress", address)
|
||||||
|
.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProxy(String proxy) {
|
||||||
|
Config config = HttpConnectConfig.parse(proxy);
|
||||||
|
m_ProxyList.clear();
|
||||||
|
m_ProxyList.add(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Config getDefaultProxy() {
|
||||||
|
if (m_ProxyList.isEmpty()) {
|
||||||
|
return HttpConnectConfig.parse("http://user1:pass1@192.168.2.10:1082");
|
||||||
|
} else {
|
||||||
|
return m_ProxyList.get(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Config getDefaultTunnelConfig(InetSocketAddress destAddress) {
|
||||||
|
return getDefaultProxy();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IPAddress getDefaultLocalIP() {
|
||||||
|
return m_IpList.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ArrayList<IPAddress> getDnsList() {
|
||||||
|
return m_DnsList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDnsTTL() {
|
||||||
|
return m_dns_ttl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getWelcomeInfo() {
|
||||||
|
return m_welcome_info;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSessionName() {
|
||||||
|
return m_session_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserAgent() {
|
||||||
|
return m_user_agent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMTU() {
|
||||||
|
return m_mtu;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetDomain(String[] items) {
|
||||||
|
m_DomainMap.clear();
|
||||||
|
addDomainToHashMap(items, 0, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addDomainToHashMap(String[] items, int offset, Boolean state) {
|
||||||
|
for (int i = offset; i < items.length; i++) {
|
||||||
|
String domainString = items[i].toLowerCase().trim();
|
||||||
|
if (domainString.length() == 0) continue;
|
||||||
|
if (domainString.charAt(0) == '.') {
|
||||||
|
domainString = domainString.substring(1);
|
||||||
|
}
|
||||||
|
m_DomainMap.put(domainString, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Boolean getDomainState(String domain) {
|
||||||
|
domain = domain.toLowerCase(Locale.ENGLISH);
|
||||||
|
while (domain.length() > 0) {
|
||||||
|
Boolean stateBoolean = m_DomainMap.get(domain);
|
||||||
|
if (stateBoolean != null) {
|
||||||
|
return stateBoolean;
|
||||||
|
} else {
|
||||||
|
int start = domain.indexOf('.') + 1;
|
||||||
|
if (start > 0 && start < domain.length()) {
|
||||||
|
domain = domain.substring(start);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean needProxy(String host) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean needProxy(int ip) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class IPAddress {
|
||||||
|
public final String Address;
|
||||||
|
public final int PrefixLength;
|
||||||
|
|
||||||
|
public IPAddress(String address, int prefixLength) {
|
||||||
|
this.Address = address;
|
||||||
|
this.PrefixLength = prefixLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IPAddress(String ipAddresString) {
|
||||||
|
String[] arrStrings = ipAddresString.split("/");
|
||||||
|
String address = arrStrings[0];
|
||||||
|
int prefixLength = 32;
|
||||||
|
if (arrStrings.length > 1) {
|
||||||
|
prefixLength = Integer.parseInt(arrStrings[1]);
|
||||||
|
}
|
||||||
|
this.Address = address;
|
||||||
|
this.PrefixLength = prefixLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format(Locale.ENGLISH, "%s/%d", Address, PrefixLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (o == null) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return this.toString().equals(o.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.vpn.core;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.tcpip.CommonMethods;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.tunnel.Tunnel;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.nio.channels.SelectionKey;
|
||||||
|
import java.nio.channels.Selector;
|
||||||
|
import java.nio.channels.ServerSocketChannel;
|
||||||
|
import java.nio.channels.SocketChannel;
|
||||||
|
import java.util.Iterator;
|
||||||
|
|
||||||
|
public class TcpProxyServer implements Runnable {
|
||||||
|
|
||||||
|
public boolean Stopped;
|
||||||
|
public short Port;
|
||||||
|
|
||||||
|
Selector m_Selector;
|
||||||
|
ServerSocketChannel m_ServerSocketChannel;
|
||||||
|
Thread m_ServerThread;
|
||||||
|
|
||||||
|
public TcpProxyServer(int port) throws IOException {
|
||||||
|
m_Selector = Selector.open();
|
||||||
|
m_ServerSocketChannel = ServerSocketChannel.open();
|
||||||
|
m_ServerSocketChannel.socket().setSoTimeout(1000*30);
|
||||||
|
m_ServerSocketChannel.configureBlocking(false);
|
||||||
|
m_ServerSocketChannel.socket().bind(new InetSocketAddress(port));
|
||||||
|
m_ServerSocketChannel.register(m_Selector, SelectionKey.OP_ACCEPT);
|
||||||
|
this.Port = (short) m_ServerSocketChannel.socket().getLocalPort();
|
||||||
|
Log.d(Constant.TAG, "AsyncTcpServer listen on " + (this.Port & 0xFFFF));
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void start() {
|
||||||
|
m_ServerThread = new Thread(this);
|
||||||
|
m_ServerThread.setName("TcpProxyServerThread");
|
||||||
|
m_ServerThread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void stop() {
|
||||||
|
this.Stopped = true;
|
||||||
|
if (m_Selector != null) {
|
||||||
|
try {
|
||||||
|
m_Selector.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(Constant.TAG, "Exception when closing m_Selector", e);
|
||||||
|
} finally {
|
||||||
|
m_Selector = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_ServerSocketChannel != null) {
|
||||||
|
try {
|
||||||
|
m_ServerSocketChannel.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(Constant.TAG, "Exception when closing m_ServerSocketChannel", e);
|
||||||
|
} finally {
|
||||||
|
m_ServerSocketChannel = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
m_Selector.select();
|
||||||
|
Iterator<SelectionKey> keyIterator = m_Selector.selectedKeys().iterator();
|
||||||
|
while (keyIterator.hasNext()) {
|
||||||
|
SelectionKey key = keyIterator.next();
|
||||||
|
if (key.isValid()) {
|
||||||
|
try {
|
||||||
|
if (key.isReadable()) {
|
||||||
|
((Tunnel) key.attachment()).onReadable(key);
|
||||||
|
} else if (key.isWritable()) {
|
||||||
|
((Tunnel) key.attachment()).onWritable(key);
|
||||||
|
} else if (key.isConnectable()) {
|
||||||
|
((Tunnel) key.attachment()).onConnectable();
|
||||||
|
} else if (key.isAcceptable()) {
|
||||||
|
onAccepted(key);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.d(Constant.TAG, e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keyIterator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(Constant.TAG, "TcpServer", e);
|
||||||
|
} finally {
|
||||||
|
this.stop();
|
||||||
|
Log.d(Constant.TAG, "TcpServer thread exited.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static InetSocketAddress getDestAddress(SocketChannel localChannel) {
|
||||||
|
short portKey = (short) localChannel.socket().getPort();
|
||||||
|
NatSession session = NatSessionManager.getSession(portKey);
|
||||||
|
if (session != null) {
|
||||||
|
if (ProxyConfig.Instance.needProxy(session.RemoteIP)) {
|
||||||
|
if (ProxyConfig.IS_DEBUG)
|
||||||
|
Log.d(Constant.TAG, String.format("%d/%d:[PROXY] %s=>%s:%d", NatSessionManager.getSessionCount(),
|
||||||
|
Tunnel.SessionCount, session.RemoteHost,
|
||||||
|
CommonMethods.ipIntToString(session.RemoteIP), session.RemotePort & 0xFFFF));
|
||||||
|
return InetSocketAddress.createUnresolved(session.RemoteHost, session.RemotePort & 0xFFFF);
|
||||||
|
} else {
|
||||||
|
return new InetSocketAddress(localChannel.socket().getInetAddress(), session.RemotePort & 0xFFFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void onAccepted(SelectionKey key) {
|
||||||
|
Tunnel localTunnel = null;
|
||||||
|
try {
|
||||||
|
SocketChannel localChannel = m_ServerSocketChannel.accept();
|
||||||
|
localTunnel = TunnelFactory.wrap(localChannel, m_Selector);
|
||||||
|
|
||||||
|
InetSocketAddress destAddress = getDestAddress(localChannel);
|
||||||
|
if (destAddress != null) {
|
||||||
|
Tunnel remoteTunnel = TunnelFactory.createTunnelByConfig(destAddress, m_Selector);
|
||||||
|
remoteTunnel.setBrotherTunnel(localTunnel);
|
||||||
|
localTunnel.setBrotherTunnel(remoteTunnel);
|
||||||
|
|
||||||
|
if (destAddress.getPort() == 80 && destAddress.getHostName().endsWith("wahlap.com")) {
|
||||||
|
destAddress = new InetSocketAddress("192.168.1.3", 3000);
|
||||||
|
remoteTunnel.connect(destAddress);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
remoteTunnel.connect(destAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// LocalVpnService.Instance.writeLog("Error: socket(%s:%d) target host is null.",
|
||||||
|
// localChannel.socket().getInetAddress().toString(), localChannel.socket().getPort());
|
||||||
|
localTunnel.dispose();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
// LocalVpnService.Instance.writeLog("Error: remote socket create failed: %s", e.toString());
|
||||||
|
if (localTunnel != null) {
|
||||||
|
localTunnel.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.vpn.core;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.server.HttpRedirectServer;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.ui.DataContext;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.tunnel.HttpCapturerTunnel;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.tunnel.RawTunnel;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.tunnel.Tunnel;
|
||||||
|
|
||||||
|
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.nio.channels.Selector;
|
||||||
|
import java.nio.channels.SocketChannel;
|
||||||
|
|
||||||
|
public class TunnelFactory {
|
||||||
|
private final static String TAG = "TunnelFactory";
|
||||||
|
|
||||||
|
public static Tunnel wrap(SocketChannel channel, Selector selector) throws Exception {
|
||||||
|
return new RawTunnel(channel, selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Tunnel createTunnelByConfig(InetSocketAddress destAddress, Selector selector) throws Exception {
|
||||||
|
Log.d(TAG, destAddress.getHostName() + ":" + destAddress.getPort());
|
||||||
|
if (destAddress.getAddress() != null)
|
||||||
|
{
|
||||||
|
Log.d(TAG, destAddress.getAddress().toString());
|
||||||
|
}
|
||||||
|
// Use online service
|
||||||
|
if (DataContext.CompatibleMode) {
|
||||||
|
if (destAddress.getHostName().endsWith("wahlap.com") && destAddress.getPort() == 80) {
|
||||||
|
Log.d(TAG, "Request for wahlap.com caught");
|
||||||
|
return new HttpCapturerTunnel(
|
||||||
|
new InetSocketAddress("127.0.0.1", HttpRedirectServer.Port), selector);
|
||||||
|
} else {
|
||||||
|
// Config config = ProxyConfig.Instance.getDefaultTunnelConfig(destAddress);
|
||||||
|
// return new HttpConnectTunnel((HttpConnectConfig) config, selector);
|
||||||
|
if (destAddress.isUnresolved())
|
||||||
|
return new RawTunnel(new InetSocketAddress(destAddress.getHostName(), destAddress.getPort()), selector);
|
||||||
|
else
|
||||||
|
return new RawTunnel(destAddress, selector);
|
||||||
|
}
|
||||||
|
// else if (destAddress.isUnresolved())
|
||||||
|
// return new RawTunnel(new InetSocketAddress(destAddress.getHostName(), destAddress.getPort()), selector);
|
||||||
|
// else
|
||||||
|
// return new RawTunnel(destAddress, selector);
|
||||||
|
}
|
||||||
|
// Use local service
|
||||||
|
else {
|
||||||
|
// if (destAddress.getHostName().endsWith(DataContext.HookHost) ||
|
||||||
|
// (destAddress.getAddress() != null && destAddress.getAddress().toString().equals(DataContext.HookHost))) {
|
||||||
|
// Log.d(TAG, "Request to" + DataContext.HookHost + " caught");
|
||||||
|
// return new RawTunnel(
|
||||||
|
// new InetSocketAddress("127.0.0.1", HttpServer.Port), selector);
|
||||||
|
// } else
|
||||||
|
if (destAddress.getHostName().endsWith("wahlap.com") && destAddress.getPort() == 80) {
|
||||||
|
Log.d(TAG, "Request for wahlap.com caught");
|
||||||
|
return new HttpCapturerTunnel(
|
||||||
|
new InetSocketAddress("127.0.0.1", HttpRedirectServer.Port), selector);
|
||||||
|
}
|
||||||
|
// else if (destAddress.getHostName().endsWith("wahlap.com") && destAddress.getPort() != 80)
|
||||||
|
// {
|
||||||
|
// Config config = ProxyConfig.Instance.getDefaultTunnelConfig(destAddress);
|
||||||
|
// return new HttpConnectTunnel((HttpConnectConfig) config, selector);
|
||||||
|
// }
|
||||||
|
else if (destAddress.isUnresolved())
|
||||||
|
return new RawTunnel(new InetSocketAddress(destAddress.getHostName(), destAddress.getPort()), selector);
|
||||||
|
else
|
||||||
|
return new RawTunnel(destAddress, selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.vpn.dns;
|
||||||
|
|
||||||
|
public class DnsFlags {
|
||||||
|
public boolean QR;//1 bits
|
||||||
|
public int OpCode;//4 bits
|
||||||
|
public boolean AA;//1 bits
|
||||||
|
public boolean TC;//1 bits
|
||||||
|
public boolean RD;//1 bits
|
||||||
|
public boolean RA;//1 bits
|
||||||
|
public int Zero;//3 bits
|
||||||
|
public int Rcode;//4 bits
|
||||||
|
|
||||||
|
public static DnsFlags Parse(short value) {
|
||||||
|
int m_Flags = value & 0xFFFF;
|
||||||
|
DnsFlags flags = new DnsFlags();
|
||||||
|
flags.QR = ((m_Flags >> 7) & 0x01) == 1;
|
||||||
|
flags.OpCode = (m_Flags >> 3) & 0x0F;
|
||||||
|
flags.AA = ((m_Flags >> 2) & 0x01) == 1;
|
||||||
|
flags.TC = ((m_Flags >> 1) & 0x01) == 1;
|
||||||
|
flags.RD = (m_Flags & 0x01) == 1;
|
||||||
|
flags.RA = (m_Flags >> 15) == 1;
|
||||||
|
flags.Zero = (m_Flags >> 12) & 0x07;
|
||||||
|
flags.Rcode = ((m_Flags >> 8) & 0xF);
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
public short ToShort() {
|
||||||
|
int m_Flags = 0;
|
||||||
|
m_Flags |= (this.QR ? 1 : 0) << 7;
|
||||||
|
m_Flags |= (this.OpCode & 0x0F) << 3;
|
||||||
|
m_Flags |= (this.AA ? 1 : 0) << 2;
|
||||||
|
m_Flags |= (this.TC ? 1 : 0) << 1;
|
||||||
|
m_Flags |= this.RD ? 1 : 0;
|
||||||
|
m_Flags |= (this.RA ? 1 : 0) << 15;
|
||||||
|
m_Flags |= (this.Zero & 0x07) << 12;
|
||||||
|
m_Flags |= (this.Rcode & 0x0F) << 8;
|
||||||
|
return (short) m_Flags;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.vpn.dns;
|
||||||
|
|
||||||
|
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.tcpip.CommonMethods;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
public class DnsHeader {
|
||||||
|
static final short offset_ID = 0;
|
||||||
|
static final short offset_Flags = 2;
|
||||||
|
static final short offset_QuestionCount = 4;
|
||||||
|
static final short offset_ResourceCount = 6;
|
||||||
|
static final short offset_AResourceCount = 8;
|
||||||
|
static final short offset_EResourceCount = 10;
|
||||||
|
public short ID;
|
||||||
|
public DnsFlags Flags;
|
||||||
|
public short QuestionCount;
|
||||||
|
public short ResourceCount;
|
||||||
|
public short AResourceCount;
|
||||||
|
public short EResourceCount;
|
||||||
|
public byte[] Data;
|
||||||
|
public int Offset;
|
||||||
|
|
||||||
|
public DnsHeader(byte[] data, int offset) {
|
||||||
|
this.Offset = offset;
|
||||||
|
this.Data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DnsHeader FromBytes(ByteBuffer buffer) {
|
||||||
|
DnsHeader header = new DnsHeader(buffer.array(), buffer.arrayOffset() + buffer.position());
|
||||||
|
header.ID = buffer.getShort();
|
||||||
|
header.Flags = DnsFlags.Parse(buffer.getShort());
|
||||||
|
header.QuestionCount = buffer.getShort();
|
||||||
|
header.ResourceCount = buffer.getShort();
|
||||||
|
header.AResourceCount = buffer.getShort();
|
||||||
|
header.EResourceCount = buffer.getShort();
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ToBytes(ByteBuffer buffer) {
|
||||||
|
buffer.putShort(this.ID);
|
||||||
|
buffer.putShort(this.Flags.ToShort());
|
||||||
|
buffer.putShort(this.QuestionCount);
|
||||||
|
buffer.putShort(this.ResourceCount);
|
||||||
|
buffer.putShort(this.AResourceCount);
|
||||||
|
buffer.putShort(this.EResourceCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getID() {
|
||||||
|
return CommonMethods.readShort(Data, Offset + offset_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setID(short value) {
|
||||||
|
CommonMethods.writeShort(Data, Offset + offset_ID, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getFlags() {
|
||||||
|
return CommonMethods.readShort(Data, Offset + offset_Flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFlags(short value) {
|
||||||
|
CommonMethods.writeShort(Data, Offset + offset_Flags, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getQuestionCount() {
|
||||||
|
return CommonMethods.readShort(Data, Offset + offset_QuestionCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setQuestionCount(short value) {
|
||||||
|
CommonMethods.writeShort(Data, Offset + offset_QuestionCount, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getResourceCount() {
|
||||||
|
return CommonMethods.readShort(Data, Offset + offset_ResourceCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setResourceCount(short value) {
|
||||||
|
CommonMethods.writeShort(Data, Offset + offset_ResourceCount, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getAResourceCount() {
|
||||||
|
return CommonMethods.readShort(Data, Offset + offset_AResourceCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAResourceCount(short value) {
|
||||||
|
CommonMethods.writeShort(Data, Offset + offset_AResourceCount, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getEResourceCount() {
|
||||||
|
return CommonMethods.readShort(Data, Offset + offset_EResourceCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEResourceCount(short value) {
|
||||||
|
CommonMethods.writeShort(Data, Offset + offset_EResourceCount, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.vpn.dns;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
public class DnsPacket {
|
||||||
|
public DnsHeader Header;
|
||||||
|
public Question[] Questions;
|
||||||
|
public Resource[] Resources;
|
||||||
|
public Resource[] AResources;
|
||||||
|
public Resource[] EResources;
|
||||||
|
|
||||||
|
public int Size;
|
||||||
|
|
||||||
|
public static DnsPacket FromBytes(ByteBuffer buffer) {
|
||||||
|
if (buffer.limit() < 12)
|
||||||
|
return null;
|
||||||
|
if (buffer.limit() > 512)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
DnsPacket packet = new DnsPacket();
|
||||||
|
packet.Size = buffer.limit();
|
||||||
|
packet.Header = DnsHeader.FromBytes(buffer);
|
||||||
|
|
||||||
|
if (packet.Header.QuestionCount > 2 || packet.Header.ResourceCount > 50 || packet.Header.AResourceCount > 50 || packet.Header.EResourceCount > 50) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
packet.Questions = new Question[packet.Header.QuestionCount];
|
||||||
|
packet.Resources = new Resource[packet.Header.ResourceCount];
|
||||||
|
packet.AResources = new Resource[packet.Header.AResourceCount];
|
||||||
|
packet.EResources = new Resource[packet.Header.EResourceCount];
|
||||||
|
|
||||||
|
for (int i = 0; i < packet.Questions.length; i++) {
|
||||||
|
packet.Questions[i] = Question.FromBytes(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < packet.Resources.length; i++) {
|
||||||
|
packet.Resources[i] = Resource.FromBytes(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < packet.AResources.length; i++) {
|
||||||
|
packet.AResources[i] = Resource.FromBytes(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < packet.EResources.length; i++) {
|
||||||
|
packet.EResources[i] = Resource.FromBytes(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String ReadDomain(ByteBuffer buffer, int dnsHeaderOffset) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
int len = 0;
|
||||||
|
while (buffer.hasRemaining() && (len = (buffer.get() & 0xFF)) > 0) {
|
||||||
|
if ((len & 0xc0) == 0xc0)// pointer <20><>2λΪ11<31><31>ʾ<EFBFBD><CABE>ָ<EFBFBD>롣<EFBFBD>磺1100 0000
|
||||||
|
{
|
||||||
|
// ָ<><D6B8><EFBFBD>ȡֵ<C8A1><D6B5>ǰһ<C7B0>ֽڵĺ<DAB5>6λ<36>Ӻ<EFBFBD>һ<EFBFBD>ֽڵ<D6BD>8λ<38><CEBB>14λ<34><CEBB>ֵ<EFBFBD><D6B5>
|
||||||
|
int pointer = buffer.get() & 0xFF;// <20><>8λ
|
||||||
|
pointer |= (len & 0x3F) << 8;// <20><>6λ
|
||||||
|
|
||||||
|
ByteBuffer newBuffer = ByteBuffer.wrap(buffer.array(), dnsHeaderOffset + pointer, dnsHeaderOffset + buffer.limit());
|
||||||
|
sb.append(ReadDomain(newBuffer, dnsHeaderOffset));
|
||||||
|
return sb.toString();
|
||||||
|
} else {
|
||||||
|
while (len > 0 && buffer.hasRemaining()) {
|
||||||
|
sb.append((char) (buffer.get() & 0xFF));
|
||||||
|
len--;
|
||||||
|
}
|
||||||
|
sb.append('.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len == 0 && sb.length() > 0) {
|
||||||
|
sb.deleteCharAt(sb.length() - 1);//ȥ<><C8A5>ĩβ<C4A9>ĵ㣨.<2E><>
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void WriteDomain(String domain, ByteBuffer buffer) {
|
||||||
|
if (domain == null || domain == "") {
|
||||||
|
buffer.put((byte) 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] arr = domain.split("\\.");
|
||||||
|
for (String item : arr) {
|
||||||
|
if (arr.length > 1) {
|
||||||
|
buffer.put((byte) item.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < item.length(); i++) {
|
||||||
|
buffer.put((byte) item.codePointAt(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ToBytes(ByteBuffer buffer) {
|
||||||
|
Header.QuestionCount = 0;
|
||||||
|
Header.ResourceCount = 0;
|
||||||
|
Header.AResourceCount = 0;
|
||||||
|
Header.EResourceCount = 0;
|
||||||
|
|
||||||
|
if (Questions != null)
|
||||||
|
Header.QuestionCount = (short) Questions.length;
|
||||||
|
if (Resources != null)
|
||||||
|
Header.ResourceCount = (short) Resources.length;
|
||||||
|
if (AResources != null)
|
||||||
|
Header.AResourceCount = (short) AResources.length;
|
||||||
|
if (EResources != null)
|
||||||
|
Header.EResourceCount = (short) EResources.length;
|
||||||
|
|
||||||
|
this.Header.ToBytes(buffer);
|
||||||
|
|
||||||
|
for (int i = 0; i < Header.QuestionCount; i++) {
|
||||||
|
this.Questions[i].ToBytes(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < Header.ResourceCount; i++) {
|
||||||
|
this.Resources[i].ToBytes(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < Header.AResourceCount; i++) {
|
||||||
|
this.AResources[i].ToBytes(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < Header.EResourceCount; i++) {
|
||||||
|
this.EResources[i].ToBytes(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.vpn.dns;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
public class Question {
|
||||||
|
public String Domain;
|
||||||
|
public short Type;
|
||||||
|
public short Class;
|
||||||
|
|
||||||
|
private int offset;
|
||||||
|
private int length;
|
||||||
|
|
||||||
|
public static Question FromBytes(ByteBuffer buffer) {
|
||||||
|
Question q = new Question();
|
||||||
|
q.offset = buffer.arrayOffset() + buffer.position();
|
||||||
|
q.Domain = DnsPacket.ReadDomain(buffer, buffer.arrayOffset());
|
||||||
|
q.Type = buffer.getShort();
|
||||||
|
q.Class = buffer.getShort();
|
||||||
|
q.length = buffer.arrayOffset() + buffer.position() - q.offset;
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Offset() {
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Length() {
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ToBytes(ByteBuffer buffer) {
|
||||||
|
this.offset = buffer.position();
|
||||||
|
DnsPacket.WriteDomain(this.Domain, buffer);
|
||||||
|
buffer.putShort(this.Type);
|
||||||
|
buffer.putShort(this.Class);
|
||||||
|
this.length = buffer.position() - this.offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.vpn.dns;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
public class Resource {
|
||||||
|
public String Domain;
|
||||||
|
public short Type;
|
||||||
|
public short Class;
|
||||||
|
public int TTL;
|
||||||
|
public short DataLength;
|
||||||
|
public byte[] Data;
|
||||||
|
|
||||||
|
private int offset;
|
||||||
|
private int length;
|
||||||
|
|
||||||
|
public static Resource FromBytes(ByteBuffer buffer) {
|
||||||
|
|
||||||
|
Resource r = new Resource();
|
||||||
|
r.offset = buffer.arrayOffset() + buffer.position();
|
||||||
|
r.Domain = DnsPacket.ReadDomain(buffer, buffer.arrayOffset());
|
||||||
|
r.Type = buffer.getShort();
|
||||||
|
r.Class = buffer.getShort();
|
||||||
|
r.TTL = buffer.getInt();
|
||||||
|
r.DataLength = buffer.getShort();
|
||||||
|
r.Data = new byte[r.DataLength & 0xFFFF];
|
||||||
|
buffer.get(r.Data);
|
||||||
|
r.length = buffer.arrayOffset() + buffer.position() - r.offset;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Offset() {
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Length() {
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ToBytes(ByteBuffer buffer) {
|
||||||
|
if (this.Data == null) {
|
||||||
|
this.Data = new byte[0];
|
||||||
|
}
|
||||||
|
this.DataLength = (short) this.Data.length;
|
||||||
|
|
||||||
|
this.offset = buffer.position();
|
||||||
|
DnsPacket.WriteDomain(this.Domain, buffer);
|
||||||
|
buffer.putShort(this.Type);
|
||||||
|
buffer.putShort(this.Class);
|
||||||
|
buffer.putInt(this.TTL);
|
||||||
|
|
||||||
|
buffer.putShort(this.DataLength);
|
||||||
|
buffer.put(this.Data);
|
||||||
|
this.length = buffer.position() - this.offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.vpn.dns;
|
||||||
|
|
||||||
|
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.tcpip.CommonMethods;
|
||||||
|
|
||||||
|
public class ResourcePointer {
|
||||||
|
static final short offset_Domain = 0;
|
||||||
|
static final short offset_Type = 2;
|
||||||
|
static final short offset_Class = 4;
|
||||||
|
static final int offset_TTL = 6;
|
||||||
|
static final short offset_DataLength = 10;
|
||||||
|
static final int offset_IP = 12;
|
||||||
|
|
||||||
|
byte[] Data;
|
||||||
|
int Offset;
|
||||||
|
|
||||||
|
public ResourcePointer(byte[] data, int offset) {
|
||||||
|
this.Data = data;
|
||||||
|
this.Offset = offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDomain(short value) {
|
||||||
|
CommonMethods.writeShort(Data, Offset + offset_Domain, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getType() {
|
||||||
|
return CommonMethods.readShort(Data, Offset + offset_Type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setType(short value) {
|
||||||
|
CommonMethods.writeShort(Data, Offset + offset_Type, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getClass(short value) {
|
||||||
|
return CommonMethods.readShort(Data, Offset + offset_Class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClass(short value) {
|
||||||
|
CommonMethods.writeShort(Data, Offset + offset_Class, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTTL() {
|
||||||
|
return CommonMethods.readInt(Data, Offset + offset_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTTL(int value) {
|
||||||
|
CommonMethods.writeInt(Data, Offset + offset_TTL, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getDataLength() {
|
||||||
|
return CommonMethods.readShort(Data, Offset + offset_DataLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDataLength(short value) {
|
||||||
|
CommonMethods.writeShort(Data, Offset + offset_DataLength, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getIP() {
|
||||||
|
return CommonMethods.readInt(Data, Offset + offset_IP);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIP(int value) {
|
||||||
|
CommonMethods.writeInt(Data, Offset + offset_IP, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.vpn.tcpip;
|
||||||
|
|
||||||
|
import java.net.Inet4Address;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
|
||||||
|
public class CommonMethods {
|
||||||
|
|
||||||
|
public static InetAddress ipIntToInet4Address(int ip) {
|
||||||
|
byte[] ipAddress = new byte[4];
|
||||||
|
writeInt(ipAddress, 0, ip);
|
||||||
|
try {
|
||||||
|
return Inet4Address.getByAddress(ipAddress);
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
// TODO Auto-generated catch block
|
||||||
|
e.printStackTrace();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String ipIntToString(int ip) {
|
||||||
|
return String.format("%s.%s.%s.%s", (ip >> 24) & 0x00FF,
|
||||||
|
(ip >> 16) & 0x00FF, (ip >> 8) & 0x00FF, ip & 0x00FF);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String ipBytesToString(byte[] ip) {
|
||||||
|
return String.format("%s.%s.%s.%s", ip[0] & 0x00FF, ip[1] & 0x00FF, ip[2] & 0x00FF, ip[3] & 0x00FF);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int ipStringToInt(String ip) {
|
||||||
|
String[] arrStrings = ip.split("\\.");
|
||||||
|
int r = (Integer.parseInt(arrStrings[0]) << 24)
|
||||||
|
| (Integer.parseInt(arrStrings[1]) << 16)
|
||||||
|
| (Integer.parseInt(arrStrings[2]) << 8)
|
||||||
|
| Integer.parseInt(arrStrings[3]);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int readInt(byte[] data, int offset) {
|
||||||
|
int r = ((data[offset] & 0xFF) << 24)
|
||||||
|
| ((data[offset + 1] & 0xFF) << 16)
|
||||||
|
| ((data[offset + 2] & 0xFF) << 8) | (data[offset + 3] & 0xFF);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static short readShort(byte[] data, int offset) {
|
||||||
|
int r = ((data[offset] & 0xFF) << 8) | (data[offset + 1] & 0xFF);
|
||||||
|
return (short) r;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void writeInt(byte[] data, int offset, int value) {
|
||||||
|
data[offset] = (byte) (value >> 24);
|
||||||
|
data[offset + 1] = (byte) (value >> 16);
|
||||||
|
data[offset + 2] = (byte) (value >> 8);
|
||||||
|
data[offset + 3] = (byte) (value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void writeShort(byte[] data, int offset, short value) {
|
||||||
|
data[offset] = (byte) (value >> 8);
|
||||||
|
data[offset + 1] = (byte) (value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// <20><><EFBFBD><EFBFBD><EFBFBD>ֽ<EFBFBD>˳<EFBFBD><CBB3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֽ<EFBFBD>˳<EFBFBD><CBB3><EFBFBD>ת<EFBFBD><D7AA>
|
||||||
|
|
||||||
|
public static short htons(short u) {
|
||||||
|
int r = ((u & 0xFFFF) << 8) | ((u & 0xFFFF) >> 8);
|
||||||
|
return (short) r;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static short ntohs(short u) {
|
||||||
|
int r = ((u & 0xFFFF) << 8) | ((u & 0xFFFF) >> 8);
|
||||||
|
return (short) r;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int hton(int u) {
|
||||||
|
int r = (u >> 24) & 0x000000FF;
|
||||||
|
r |= (u >> 8) & 0x0000FF00;
|
||||||
|
r |= (u << 8) & 0x00FF0000;
|
||||||
|
r |= (u << 24) & 0xFF000000;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int ntoh(int u) {
|
||||||
|
int r = (u >> 24) & 0x000000FF;
|
||||||
|
r |= (u >> 8) & 0x0000FF00;
|
||||||
|
r |= (u << 8) & 0x00FF0000;
|
||||||
|
r |= (u << 24) & 0xFF000000;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// <20><><EFBFBD><EFBFBD>У<EFBFBD><D0A3><EFBFBD>
|
||||||
|
public static short checksum(long sum, byte[] buf, int offset, int len) {
|
||||||
|
sum += getsum(buf, offset, len);
|
||||||
|
while ((sum >> 16) > 0)
|
||||||
|
sum = (sum & 0xFFFF) + (sum >> 16);
|
||||||
|
return (short) ~sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long getsum(byte[] buf, int offset, int len) {
|
||||||
|
long sum = 0; /* assume 32 bit long, 16 bit short */
|
||||||
|
while (len > 1) {
|
||||||
|
sum += readShort(buf, offset) & 0xFFFF;
|
||||||
|
offset += 2;
|
||||||
|
len -= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len > 0) /* take care of left over byte */ {
|
||||||
|
sum += (buf[offset] & 0xFF) << 8;
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
// <20><><EFBFBD><EFBFBD>IP<49><50><EFBFBD>У<EFBFBD><D0A3><EFBFBD>
|
||||||
|
public static boolean ComputeIPChecksum(IPHeader ipHeader) {
|
||||||
|
short oldCrc = ipHeader.getCrc();
|
||||||
|
ipHeader.setCrc((short) 0);// <20><><EFBFBD><EFBFBD>ǰ<EFBFBD><C7B0><EFBFBD><EFBFBD>
|
||||||
|
short newCrc = CommonMethods.checksum(0, ipHeader.m_Data,
|
||||||
|
ipHeader.m_Offset, ipHeader.getHeaderLength());
|
||||||
|
ipHeader.setCrc(newCrc);
|
||||||
|
return oldCrc == newCrc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// <20><><EFBFBD><EFBFBD>TCP<43><50>UDP<44><50>У<EFBFBD><D0A3><EFBFBD>
|
||||||
|
public static boolean ComputeTCPChecksum(IPHeader ipHeader, TCPHeader tcpHeader) {
|
||||||
|
ComputeIPChecksum(ipHeader);//<2F><><EFBFBD><EFBFBD>IPУ<50><D0A3><EFBFBD>
|
||||||
|
int ipData_len = ipHeader.getTotalLength() - ipHeader.getHeaderLength();// IP<49><50>ݳ<EFBFBD><DDB3><EFBFBD>
|
||||||
|
if (ipData_len < 0)
|
||||||
|
return false;
|
||||||
|
// <20><><EFBFBD><EFBFBD>Ϊα<CEAA>ײ<EFBFBD><D7B2><EFBFBD>
|
||||||
|
long sum = getsum(ipHeader.m_Data, ipHeader.m_Offset
|
||||||
|
+ IPHeader.offset_src_ip, 8);
|
||||||
|
sum += ipHeader.getProtocol() & 0xFF;
|
||||||
|
sum += ipData_len;
|
||||||
|
|
||||||
|
short oldCrc = tcpHeader.getCrc();
|
||||||
|
tcpHeader.setCrc((short) 0);// <20><><EFBFBD><EFBFBD>ǰ<EFBFBD><C7B0>0
|
||||||
|
|
||||||
|
short newCrc = checksum(sum, tcpHeader.m_Data, tcpHeader.m_Offset, ipData_len);// <20><><EFBFBD><EFBFBD>У<EFBFBD><D0A3><EFBFBD>
|
||||||
|
|
||||||
|
tcpHeader.setCrc(newCrc);
|
||||||
|
return oldCrc == newCrc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// <20><><EFBFBD><EFBFBD>TCP<43><50>UDP<44><50>У<EFBFBD><D0A3><EFBFBD>
|
||||||
|
public static boolean ComputeUDPChecksum(IPHeader ipHeader, UDPHeader udpHeader) {
|
||||||
|
ComputeIPChecksum(ipHeader);//<2F><><EFBFBD><EFBFBD>IPУ<50><D0A3><EFBFBD>
|
||||||
|
int ipData_len = ipHeader.getTotalLength() - ipHeader.getHeaderLength();// IP<49><50>ݳ<EFBFBD><DDB3><EFBFBD>
|
||||||
|
if (ipData_len < 0)
|
||||||
|
return false;
|
||||||
|
// <20><><EFBFBD><EFBFBD>Ϊα<CEAA>ײ<EFBFBD><D7B2><EFBFBD>
|
||||||
|
long sum = getsum(ipHeader.m_Data, ipHeader.m_Offset
|
||||||
|
+ IPHeader.offset_src_ip, 8);
|
||||||
|
sum += ipHeader.getProtocol() & 0xFF;
|
||||||
|
sum += ipData_len;
|
||||||
|
|
||||||
|
short oldCrc = udpHeader.getCrc();
|
||||||
|
udpHeader.setCrc((short) 0);// <20><><EFBFBD><EFBFBD>ǰ<EFBFBD><C7B0>0
|
||||||
|
|
||||||
|
short newCrc = checksum(sum, udpHeader.m_Data, udpHeader.m_Offset, ipData_len);// <20><><EFBFBD><EFBFBD>У<EFBFBD><D0A3><EFBFBD>
|
||||||
|
|
||||||
|
udpHeader.setCrc(newCrc);
|
||||||
|
return oldCrc == newCrc;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.vpn.tcpip;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public class IPHeader {
|
||||||
|
|
||||||
|
public static final short IP = 0x0800;
|
||||||
|
public static final byte ICMP = 1;
|
||||||
|
public static final byte TCP = 6;
|
||||||
|
public static final byte UDP = 17;
|
||||||
|
public static final byte offset_proto = 9; // 9: Protocol
|
||||||
|
public static final int offset_src_ip = 12; // 12: Source address
|
||||||
|
public static final int offset_dest_ip = 16; // 16: Destination address
|
||||||
|
static final byte offset_ver_ihl = 0; // 0: Version (4 bits) + Internet header length (4// bits)
|
||||||
|
static final byte offset_tos = 1; // 1: Type of service
|
||||||
|
static final short offset_tlen = 2; // 2: Total length
|
||||||
|
static final short offset_identification = 4; // :4 Identification
|
||||||
|
static final short offset_flags_fo = 6; // 6: Flags (3 bits) + Fragment offset (13 bits)
|
||||||
|
static final byte offset_ttl = 8; // 8: Time to live
|
||||||
|
static final short offset_crc = 10; // 10: Header checksum
|
||||||
|
static final int offset_op_pad = 20; // 20: Option + Padding
|
||||||
|
|
||||||
|
public byte[] m_Data;
|
||||||
|
public int m_Offset;
|
||||||
|
|
||||||
|
public IPHeader(byte[] data, int offset) {
|
||||||
|
this.m_Data = data;
|
||||||
|
this.m_Offset = offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Default() {
|
||||||
|
setHeaderLength(20);
|
||||||
|
setTos((byte) 0);
|
||||||
|
setTotalLength(0);
|
||||||
|
setIdentification(0);
|
||||||
|
setFlagsAndOffset((short) 0);
|
||||||
|
setTTL((byte) 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDataLength() {
|
||||||
|
return this.getTotalLength() - this.getHeaderLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getHeaderLength() {
|
||||||
|
return (m_Data[m_Offset + offset_ver_ihl] & 0x0F) * 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHeaderLength(int value) {
|
||||||
|
m_Data[m_Offset + offset_ver_ihl] = (byte) ((4 << 4) | (value / 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte getTos() {
|
||||||
|
return m_Data[m_Offset + offset_tos];
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTos(byte value) {
|
||||||
|
m_Data[m_Offset + offset_tos] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTotalLength() {
|
||||||
|
return CommonMethods.readShort(m_Data, m_Offset + offset_tlen) & 0xFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotalLength(int value) {
|
||||||
|
CommonMethods.writeShort(m_Data, m_Offset + offset_tlen, (short) value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getIdentification() {
|
||||||
|
return CommonMethods.readShort(m_Data, m_Offset + offset_identification) & 0xFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIdentification(int value) {
|
||||||
|
CommonMethods.writeShort(m_Data, m_Offset + offset_identification, (short) value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getFlagsAndOffset() {
|
||||||
|
return CommonMethods.readShort(m_Data, m_Offset + offset_flags_fo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFlagsAndOffset(short value) {
|
||||||
|
CommonMethods.writeShort(m_Data, m_Offset + offset_flags_fo, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte getTTL() {
|
||||||
|
return m_Data[m_Offset + offset_ttl];
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTTL(byte value) {
|
||||||
|
m_Data[m_Offset + offset_ttl] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte getProtocol() {
|
||||||
|
return m_Data[m_Offset + offset_proto];
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProtocol(byte value) {
|
||||||
|
m_Data[m_Offset + offset_proto] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getCrc() {
|
||||||
|
return CommonMethods.readShort(m_Data, m_Offset + offset_crc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCrc(short value) {
|
||||||
|
CommonMethods.writeShort(m_Data, m_Offset + offset_crc, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSourceIP() {
|
||||||
|
return CommonMethods.readInt(m_Data, m_Offset + offset_src_ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSourceIP(int value) {
|
||||||
|
CommonMethods.writeInt(m_Data, m_Offset + offset_src_ip, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDestinationIP() {
|
||||||
|
return CommonMethods.readInt(m_Data, m_Offset + offset_dest_ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDestinationIP(int value) {
|
||||||
|
CommonMethods.writeInt(m_Data, m_Offset + offset_dest_ip, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format(Locale.ENGLISH, "%s->%s Pro=%s,HLen=%d", CommonMethods.ipIntToString(getSourceIP()), CommonMethods.ipIntToString(getDestinationIP()), getProtocol(), getHeaderLength());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.vpn.tcpip;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public class TCPHeader {
|
||||||
|
|
||||||
|
public static final int FIN = 1;
|
||||||
|
public static final int SYN = 2;
|
||||||
|
public static final int RST = 4;
|
||||||
|
public static final int PSH = 8;
|
||||||
|
public static final int ACK = 16;
|
||||||
|
public static final int URG = 32;
|
||||||
|
|
||||||
|
static final short offset_src_port = 0;
|
||||||
|
static final short offset_dest_port = 2;
|
||||||
|
static final int offset_seq = 4;
|
||||||
|
static final int offset_ack = 8;
|
||||||
|
static final byte offset_lenres = 12;
|
||||||
|
static final byte offset_flag = 13;
|
||||||
|
static final short offset_win = 14;
|
||||||
|
static final short offset_crc = 16;
|
||||||
|
static final short offset_urp = 18;
|
||||||
|
|
||||||
|
public byte[] m_Data;
|
||||||
|
public int m_Offset;
|
||||||
|
|
||||||
|
public TCPHeader(byte[] data, int offset) {
|
||||||
|
this.m_Data = data;
|
||||||
|
this.m_Offset = offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getHeaderLength() {
|
||||||
|
int lenres = m_Data[m_Offset + offset_lenres] & 0xFF;
|
||||||
|
return (lenres >> 4) * 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getSourcePort() {
|
||||||
|
return CommonMethods.readShort(m_Data, m_Offset + offset_src_port);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSourcePort(short value) {
|
||||||
|
CommonMethods.writeShort(m_Data, m_Offset + offset_src_port, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getDestinationPort() {
|
||||||
|
return CommonMethods.readShort(m_Data, m_Offset + offset_dest_port);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDestinationPort(short value) {
|
||||||
|
CommonMethods.writeShort(m_Data, m_Offset + offset_dest_port, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte getFlags() {
|
||||||
|
return m_Data[m_Offset + offset_flag];
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getCrc() {
|
||||||
|
return CommonMethods.readShort(m_Data, m_Offset + offset_crc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCrc(short value) {
|
||||||
|
CommonMethods.writeShort(m_Data, m_Offset + offset_crc, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSeqID() {
|
||||||
|
return CommonMethods.readInt(m_Data, m_Offset + offset_seq);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAckID() {
|
||||||
|
return CommonMethods.readInt(m_Data, m_Offset + offset_ack);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
return String.format(Locale.ENGLISH, "%s%s%s%s%s%s%d->%d %s:%s",
|
||||||
|
(getFlags() & SYN) == SYN ? "SYN " : "",
|
||||||
|
(getFlags() & ACK) == ACK ? "ACK " : "",
|
||||||
|
(getFlags() & PSH) == PSH ? "PSH " : "",
|
||||||
|
(getFlags() & RST) == RST ? "RST " : "",
|
||||||
|
(getFlags() & FIN) == FIN ? "FIN " : "",
|
||||||
|
(getFlags() & URG) == URG ? "URG " : "",
|
||||||
|
getSourcePort() & 0xFFFF,
|
||||||
|
getDestinationPort() & 0xFFFF,
|
||||||
|
getSeqID(),
|
||||||
|
getAckID());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.vpn.tcpip;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public class UDPHeader {
|
||||||
|
static final short offset_src_port = 0; // Source port
|
||||||
|
static final short offset_dest_port = 2; // Destination port
|
||||||
|
static final short offset_tlen = 4; // Datagram length
|
||||||
|
static final short offset_crc = 6; // Checksum
|
||||||
|
|
||||||
|
public byte[] m_Data;
|
||||||
|
public int m_Offset;
|
||||||
|
|
||||||
|
public UDPHeader(byte[] data, int offset) {
|
||||||
|
this.m_Data = data;
|
||||||
|
this.m_Offset = offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getSourcePort() {
|
||||||
|
return CommonMethods.readShort(m_Data, m_Offset + offset_src_port);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSourcePort(short value) {
|
||||||
|
CommonMethods.writeShort(m_Data, m_Offset + offset_src_port, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getDestinationPort() {
|
||||||
|
return CommonMethods.readShort(m_Data, m_Offset + offset_dest_port);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDestinationPort(short value) {
|
||||||
|
CommonMethods.writeShort(m_Data, m_Offset + offset_dest_port, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTotalLength() {
|
||||||
|
return CommonMethods.readShort(m_Data, m_Offset + offset_tlen) & 0xFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotalLength(int value) {
|
||||||
|
CommonMethods.writeShort(m_Data, m_Offset + offset_tlen, (short) value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getCrc() {
|
||||||
|
return CommonMethods.readShort(m_Data, m_Offset + offset_crc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCrc(short value) {
|
||||||
|
CommonMethods.writeShort(m_Data, m_Offset + offset_crc, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
return String.format(Locale.ENGLISH, "%d->%d", getSourcePort() & 0xFFFF,
|
||||||
|
getDestinationPort() & 0xFFFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.vpn.tunnel;
|
||||||
|
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
|
||||||
|
public abstract class Config {
|
||||||
|
public InetSocketAddress ServerAddress;
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.vpn.tunnel;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.crawler.CrawlerCaller;
|
||||||
|
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.channels.Selector;
|
||||||
|
import java.nio.channels.SocketChannel;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public class HttpCapturerTunnel extends Tunnel {
|
||||||
|
private static final String TAG = "HttpCapturerTunnel";
|
||||||
|
|
||||||
|
public HttpCapturerTunnel(InetSocketAddress serverAddress, Selector selector) throws Exception {
|
||||||
|
super(serverAddress, selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpCapturerTunnel(SocketChannel innerChannel, Selector selector) throws Exception {
|
||||||
|
super(innerChannel, selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onConnected(ByteBuffer buffer) throws Exception {
|
||||||
|
onTunnelEstablished();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void beforeSend(ByteBuffer buffer) throws Exception {
|
||||||
|
String body = new String(buffer.array());
|
||||||
|
if (!body.contains("HTTP")) return;
|
||||||
|
|
||||||
|
// Extract http target from http packet
|
||||||
|
String[] lines = body.split("\r\n");
|
||||||
|
String path = lines[0].split(" ")[1];
|
||||||
|
String host = "";
|
||||||
|
for (String line : lines) {
|
||||||
|
if (line.toLowerCase(Locale.ROOT).startsWith("host")) {
|
||||||
|
host = line.substring(4);
|
||||||
|
while (host.startsWith(":") || host.startsWith(" ")) {
|
||||||
|
host = host.substring(1);
|
||||||
|
}
|
||||||
|
while (host.endsWith("\n") || host.endsWith("\r") || host.endsWith(" ")) {
|
||||||
|
host = host.substring(0, host.length() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!path.startsWith("/")) path = "/" + path;
|
||||||
|
|
||||||
|
String url = "http://" + host + path;
|
||||||
|
Log.d(TAG, "HTTP url: " + url);
|
||||||
|
|
||||||
|
// If it's a auth redirect request, catch it
|
||||||
|
if (url.startsWith("http://tgk-wcaime.wahlap.com/wc_auth/oauth/callback/maimai-dx")) {
|
||||||
|
Log.d(TAG, "Auth request caught!");
|
||||||
|
CrawlerCaller.fetchData(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void afterReceived(ByteBuffer buffer) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isTunnelEstablished() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDispose() {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.vpn.tunnel;
|
||||||
|
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.channels.Selector;
|
||||||
|
import java.nio.channels.SocketChannel;
|
||||||
|
|
||||||
|
public class RawTunnel extends Tunnel {
|
||||||
|
|
||||||
|
public RawTunnel(InetSocketAddress serverAddress, Selector selector) throws Exception {
|
||||||
|
super(serverAddress, selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RawTunnel(SocketChannel innerChannel, Selector selector) throws Exception {
|
||||||
|
super(innerChannel, selector);
|
||||||
|
// TODO Auto-generated constructor stub
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onConnected(ByteBuffer buffer) throws Exception {
|
||||||
|
onTunnelEstablished();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void beforeSend(ByteBuffer buffer) throws Exception {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void afterReceived(ByteBuffer buffer) throws Exception {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isTunnelEstablished() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDispose() {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.vpn.tunnel;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.util.Log;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.core.Constant;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.core.LocalVpnService;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.core.ProxyConfig;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.channels.SelectionKey;
|
||||||
|
import java.nio.channels.Selector;
|
||||||
|
import java.nio.channels.SocketChannel;
|
||||||
|
public abstract class Tunnel {
|
||||||
|
|
||||||
|
public static long SessionCount;
|
||||||
|
protected InetSocketAddress m_DestAddress;
|
||||||
|
public SocketChannel m_InnerChannel;
|
||||||
|
private ByteBuffer m_SendRemainBuffer;
|
||||||
|
private Selector m_Selector;
|
||||||
|
public Tunnel m_BrotherTunnel;
|
||||||
|
private boolean m_Disposed;
|
||||||
|
private InetSocketAddress m_ServerEP;
|
||||||
|
public Tunnel(SocketChannel innerChannel, Selector selector) throws IOException {
|
||||||
|
this.m_InnerChannel = innerChannel;
|
||||||
|
this.m_InnerChannel.socket().setSoTimeout(1000*30);
|
||||||
|
this.m_Selector = selector;
|
||||||
|
SessionCount++;
|
||||||
|
}
|
||||||
|
public Tunnel(InetSocketAddress serverAddress, Selector selector) throws IOException {
|
||||||
|
SocketChannel innerChannel = SocketChannel.open();
|
||||||
|
innerChannel.configureBlocking(false);
|
||||||
|
this.m_InnerChannel = innerChannel;
|
||||||
|
this.m_InnerChannel.socket().setSoTimeout(1000*30);
|
||||||
|
this.m_Selector = selector;
|
||||||
|
this.m_ServerEP = serverAddress;
|
||||||
|
SessionCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void onConnected(ByteBuffer buffer) throws Exception;
|
||||||
|
|
||||||
|
protected abstract boolean isTunnelEstablished();
|
||||||
|
|
||||||
|
protected abstract void beforeSend(ByteBuffer buffer) throws Exception;
|
||||||
|
|
||||||
|
protected abstract void afterReceived(ByteBuffer buffer) throws Exception;
|
||||||
|
|
||||||
|
protected abstract void onDispose();
|
||||||
|
|
||||||
|
public void setBrotherTunnel(Tunnel brotherTunnel) {
|
||||||
|
m_BrotherTunnel = brotherTunnel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void connect(InetSocketAddress destAddress) throws Exception {
|
||||||
|
if (LocalVpnService.Instance.protect(m_InnerChannel.socket())) {
|
||||||
|
m_DestAddress = destAddress;
|
||||||
|
m_InnerChannel.register(m_Selector, SelectionKey.OP_CONNECT, this);
|
||||||
|
m_InnerChannel.connect(m_ServerEP);
|
||||||
|
} else {
|
||||||
|
throw new Exception("VPN protect socket failed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void beginReceive() throws Exception {
|
||||||
|
if (m_InnerChannel.isBlocking()) {
|
||||||
|
m_InnerChannel.configureBlocking(false);
|
||||||
|
}
|
||||||
|
m_InnerChannel.register(m_Selector, SelectionKey.OP_READ, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected boolean write(ByteBuffer buffer, boolean copyRemainData) throws Exception {
|
||||||
|
int bytesSent;
|
||||||
|
while (buffer.hasRemaining()) {
|
||||||
|
bytesSent = m_InnerChannel.write(buffer);
|
||||||
|
if (bytesSent == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buffer.hasRemaining()) {
|
||||||
|
if (copyRemainData) {
|
||||||
|
if (m_SendRemainBuffer == null) {
|
||||||
|
m_SendRemainBuffer = ByteBuffer.allocate(buffer.capacity());
|
||||||
|
}
|
||||||
|
m_SendRemainBuffer.clear();
|
||||||
|
m_SendRemainBuffer.put(buffer);
|
||||||
|
m_SendRemainBuffer.flip();
|
||||||
|
m_InnerChannel.register(m_Selector, SelectionKey.OP_WRITE, this);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onTunnelEstablished() throws Exception {
|
||||||
|
this.beginReceive();
|
||||||
|
m_BrotherTunnel.beginReceive();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("DefaultLocale")
|
||||||
|
public void onConnectable() {
|
||||||
|
try {
|
||||||
|
if (m_InnerChannel.finishConnect()) {
|
||||||
|
onConnected(ByteBuffer.allocate(2048));
|
||||||
|
} else {
|
||||||
|
// LocalVpnService.Instance.writeLog("Error: connect to %s failed.", m_ServerEP);
|
||||||
|
this.dispose();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// LocalVpnService.Instance.writeLog("Error: connect to %s failed: %s", m_ServerEP, e);
|
||||||
|
this.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onReadable(SelectionKey key) {
|
||||||
|
try {
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(2048);
|
||||||
|
buffer.clear();
|
||||||
|
int bytesRead = m_InnerChannel.read(buffer);
|
||||||
|
if (bytesRead > 0) {
|
||||||
|
buffer.flip();
|
||||||
|
afterReceived(buffer);
|
||||||
|
if (isTunnelEstablished() && buffer.hasRemaining()) {
|
||||||
|
m_BrotherTunnel.beforeSend(buffer);
|
||||||
|
if (!m_BrotherTunnel.write(buffer, true)) {
|
||||||
|
key.cancel();
|
||||||
|
if (ProxyConfig.IS_DEBUG)
|
||||||
|
Log.d(Constant.TAG, m_ServerEP + "can not read more.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (bytesRead < 0) {
|
||||||
|
this.dispose();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
this.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onWritable(SelectionKey key) {
|
||||||
|
try {
|
||||||
|
this.beforeSend(m_SendRemainBuffer);
|
||||||
|
if (this.write(m_SendRemainBuffer, false)) {
|
||||||
|
key.cancel();
|
||||||
|
if (isTunnelEstablished()) {
|
||||||
|
m_BrotherTunnel.beginReceive();
|
||||||
|
} else {
|
||||||
|
this.beginReceive();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
this.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void dispose() {
|
||||||
|
disposeInternal(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void disposeInternal(boolean disposeBrother) {
|
||||||
|
if (m_Disposed) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
m_InnerChannel.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_BrotherTunnel != null && disposeBrother) {
|
||||||
|
m_BrotherTunnel.disposeInternal(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_InnerChannel = null;
|
||||||
|
m_SendRemainBuffer = null;
|
||||||
|
m_Selector = null;
|
||||||
|
m_BrotherTunnel = null;
|
||||||
|
m_Disposed = true;
|
||||||
|
SessionCount--;
|
||||||
|
|
||||||
|
onDispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.vpn.tunnel.httpconnect;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.tunnel.Config;
|
||||||
|
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
public class HttpConnectConfig extends Config {
|
||||||
|
public String UserName;
|
||||||
|
public String Password;
|
||||||
|
|
||||||
|
public static HttpConnectConfig parse(String proxyInfo) {
|
||||||
|
HttpConnectConfig config = new HttpConnectConfig();
|
||||||
|
Uri uri = Uri.parse(proxyInfo);
|
||||||
|
String userInfoString = uri.getUserInfo();
|
||||||
|
if (userInfoString != null) {
|
||||||
|
String[] userStrings = userInfoString.split(":");
|
||||||
|
config.UserName = userStrings[0];
|
||||||
|
if (userStrings.length >= 2) {
|
||||||
|
config.Password = userStrings[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
config.ServerAddress = new InetSocketAddress(uri.getHost(), uri.getPort());
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (o == null)
|
||||||
|
return false;
|
||||||
|
return this.toString().equals(o.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("http://%s:%s@%s", UserName, Password, ServerAddress);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package org.astral.findmaimaiultra.utill.updater.vpn.tunnel.httpconnect;
|
||||||
|
|
||||||
|
import android.util.Base64;
|
||||||
|
import android.util.Log;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.ui.DataContext;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.core.ProxyConfig;
|
||||||
|
import org.astral.findmaimaiultra.utill.updater.vpn.tunnel.Tunnel;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.channels.Selector;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
|
||||||
|
public class HttpConnectTunnel extends Tunnel {
|
||||||
|
private static final String TAG = "HttpConnectTunnel";
|
||||||
|
private boolean m_TunnelEstablished;
|
||||||
|
private boolean m_FirstPacket;
|
||||||
|
private HttpConnectConfig m_Config;
|
||||||
|
|
||||||
|
public HttpConnectTunnel(HttpConnectConfig config, Selector selector) throws IOException {
|
||||||
|
// super(config.ServerAddress, selector);
|
||||||
|
super(new InetSocketAddress(DataContext.ProxyHost, DataContext.ProxyPort), selector);
|
||||||
|
m_Config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onConnected(ByteBuffer buffer) throws Exception {
|
||||||
|
String request;
|
||||||
|
// if (TextUtils.isEmpty(m_Config.UserName) || TextUtils.isEmpty(m_Config.Password)) {
|
||||||
|
request = String.format(Locale.ENGLISH, "CONNECT %s:%d HTTP/1.0\r\n" +
|
||||||
|
"Proxy-Connection: keep-alive\r\n" +
|
||||||
|
"User-Agent: %s\r\n" +
|
||||||
|
"X-App-Install-ID: %s" +
|
||||||
|
"\r\n\r\n",
|
||||||
|
m_DestAddress.getHostName(),
|
||||||
|
m_DestAddress.getPort(),
|
||||||
|
ProxyConfig.Instance.getUserAgent(),
|
||||||
|
ProxyConfig.AppInstallID);
|
||||||
|
// } else {
|
||||||
|
// request = String.format(Locale.ENGLISH, "CONNECT %s:%d HTTP/1.0\r\n" +
|
||||||
|
// "Proxy-Authorization: Basic %s\r\n" +
|
||||||
|
// "Proxy-Connection: keep-alive\r\n" +
|
||||||
|
// "User-Agent: %s\r\n" +
|
||||||
|
// "X-App-Install-ID: %s" +
|
||||||
|
// "\r\n\r\n",
|
||||||
|
// m_DestAddress.getHostName(),
|
||||||
|
// m_DestAddress.getPort(),
|
||||||
|
// makeAuthorization(),
|
||||||
|
// ProxyConfig.Instance.getUserAgent(),
|
||||||
|
// ProxyConfig.AppInstallID);
|
||||||
|
// }
|
||||||
|
Log.i(TAG, "onConnected: " + request);
|
||||||
|
buffer.clear();
|
||||||
|
buffer.put(request.getBytes());
|
||||||
|
buffer.flip();
|
||||||
|
if (this.write(buffer, true)) {
|
||||||
|
this.beginReceive();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String makeAuthorization() {
|
||||||
|
return Base64.encodeToString((m_Config.UserName + ":" + m_Config.Password).getBytes(), Base64.DEFAULT).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void afterReceived(ByteBuffer buffer) throws Exception {
|
||||||
|
if (!m_TunnelEstablished) {
|
||||||
|
String response = new String(buffer.array(), buffer.position(), 12);
|
||||||
|
Log.i(TAG, m_DestAddress.toString());
|
||||||
|
Log.i(TAG, "afterReceived: " + response);
|
||||||
|
if (response.matches("^HTTP/1.[01] 200$")) {
|
||||||
|
buffer.limit(buffer.position());
|
||||||
|
} else {
|
||||||
|
throw new Exception(String.format(Locale.ENGLISH, "Proxy server responsed an error: %s", response));
|
||||||
|
}
|
||||||
|
|
||||||
|
m_TunnelEstablished = true;
|
||||||
|
m_FirstPacket = true;
|
||||||
|
super.onTunnelEstablished();
|
||||||
|
} else if (m_FirstPacket) {
|
||||||
|
// Workaround for mysterious "Content-Length: 0" after handshaking.
|
||||||
|
// Possible a bug of golang.
|
||||||
|
// Also need to remove "\r\n" afterward.
|
||||||
|
String response = new String(buffer.array(), buffer.position(), 17);
|
||||||
|
if (response.matches("^Content-Length: 0$")) {
|
||||||
|
buffer.position(buffer.position() + 17);
|
||||||
|
}
|
||||||
|
while (true) {
|
||||||
|
response = new String(buffer.array(), buffer.position(), 2);
|
||||||
|
if (response.matches("^\r\n$")) {
|
||||||
|
buffer.position(buffer.position() + 2);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_FirstPacket = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isTunnelEstablished() {
|
||||||
|
return m_TunnelEstablished;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void beforeSend(ByteBuffer buffer) throws Exception {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDispose() {
|
||||||
|
m_Config = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
BIN
app/src/main/res/drawable/ap.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
app/src/main/res/drawable/app.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
app/src/main/res/drawable/fc.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
app/src/main/res/drawable/fcp.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
app/src/main/res/drawable/fs.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
app/src/main/res/drawable/fsd.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
app/src/main/res/drawable/fsdp.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
app/src/main/res/drawable/fsp.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
app/src/main/res/drawable/sync.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
125
app/src/main/res/layout/activity_paika.xml
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/colorOnPrimary"
|
||||||
|
android:fillViewport="true">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:id="@+id/enterParty">
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingTop="16dp"
|
||||||
|
android:paddingBottom="16dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:paddingEnd="8dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/party"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="PartyName" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="进入或创建房间"
|
||||||
|
android:id="@+id/enter"/>
|
||||||
|
</LinearLayout>
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingTop="16dp"
|
||||||
|
android:paddingBottom="16dp">
|
||||||
|
|
||||||
|
<!-- 输入框,占据剩余空间 -->
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:paddingEnd="8dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/name"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="输入您的昵称(方便别人识别)" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:layout_width="100dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:text="Default"
|
||||||
|
android:id="@+id/card"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:id="@+id/joinParty">
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:id="@+id/partyHouse"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="@color/textcolorPrimary"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="排卡"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:id="@+id/add"/>
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/red"
|
||||||
|
android:text="退勤"
|
||||||
|
android:id="@+id/leave"/>
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:text="上机"
|
||||||
|
android:id="@+id/play"/>
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
<TableLayout
|
||||||
|
android:id="@+id/tableLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:stretchColumns="*">
|
||||||
|
</TableLayout>
|
||||||
|
</ScrollView>
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
175
app/src/main/res/layout/activity_update.xml
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:background="@color/colorPrimary"
|
||||||
|
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
|
||||||
|
app:cardElevation="4dp"
|
||||||
|
app:popupTheme="@style/ThemeOverlay.AppCompat.Light">
|
||||||
|
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="启动"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:layout_marginLeft="25mm"/>
|
||||||
|
</androidx.appcompat.widget.Toolbar>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/username"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/username"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ems="10"
|
||||||
|
android:hint="Username"
|
||||||
|
android:inputType="textPersonName" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView3"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/password"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/password"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:autofillHints="password"
|
||||||
|
android:ems="10"
|
||||||
|
android:hint="Password"
|
||||||
|
android:inputType="textPassword" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
android:id="@+id/copyUrl"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:checked="true"
|
||||||
|
android:text="复制链接" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
android:id="@+id/autoLaunch"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="0.75"
|
||||||
|
android:checked="true"
|
||||||
|
android:text="自动打开微信" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/basic"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:checked="false"
|
||||||
|
android:text="Basic"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/advanced"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:checked="false"
|
||||||
|
android:text="Advanced"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/expert"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:checked="true"
|
||||||
|
android:text="Expert"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/master"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:checked="true"
|
||||||
|
android:text="Master"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/remaster"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:checked="true"
|
||||||
|
android:text="Re:Mas"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
<LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal">
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:onClick="saveText"
|
||||||
|
android:text="Save" />
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button2"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:onClick="saveText"
|
||||||
|
android:text="注册水鱼账号" />
|
||||||
|
<Button
|
||||||
|
android:id="@+id/sy2lx"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:onClick="saveText"
|
||||||
|
android:text="水鱼数据迁移到落雪" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textViewLog"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:fadeScrollbars="false"
|
||||||
|
android:padding="13dp"
|
||||||
|
android:scrollbarStyle="outsideInset"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||||
|
android:textColor="@color/secondary_text"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:visibility="visible"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -23,13 +23,4 @@
|
|||||||
|
|
||||||
<include layout="@layout/content_main"/>
|
<include layout="@layout/content_main"/>
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:id="@+id/fab"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="bottom|end"
|
|
||||||
android:layout_marginEnd="@dimen/fab_margin"
|
|
||||||
android:layout_marginBottom="16dp"
|
|
||||||
app:srcCompat="@android:drawable/ic_dialog_email"/>
|
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
23
app/src/main/res/layout/dialog_card_style.xml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<!-- res/layout/dialog_card_style.xml -->
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:id="@+id/layout_buttons"
|
||||||
|
android:padding="8dp">
|
||||||
|
|
||||||
|
<!-- 动态添加的按钮将放在这里 -->
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
|
</LinearLayout>
|
||||||
29
app/src/main/res/layout/fragment_music.xml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".ui.music.MusicFragment">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recyclerView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
android:id="@+id/fab"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:background="@color/colorSecondary"
|
||||||
|
android:contentDescription="123"
|
||||||
|
android:src="@drawable/id_add"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
tools:ignore="MissingConstraints" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
54
app/src/main/res/layout/item_music_rating.xml
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:cardElevation="4dp"
|
||||||
|
android:layout_margin="8dp">
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/backgroundLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:alpha="0.5" /> <!-- 设置透明度为50% -->
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/musicName"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/level"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/ach"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/achievementImage"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:scaleType="centerInside" />
|
||||||
|
</LinearLayout>
|
||||||
|
</RelativeLayout>
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
83
app/src/main/res/layout/music_dialog.xml
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<?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="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/dialog_music_image"
|
||||||
|
android:layout_width="100dp"
|
||||||
|
android:layout_height="100dp"
|
||||||
|
android:scaleType="centerCrop"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingStart="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/dialog_music_name"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="歌曲名称"
|
||||||
|
android:textColor="@color/primary"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/dialog_music_achievement"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/primary"
|
||||||
|
|
||||||
|
android:text="达成率"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/dialog_music_rating"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/primary"
|
||||||
|
|
||||||
|
android:text="评分"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/dialog_music_level_info"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/primary"
|
||||||
|
|
||||||
|
android:text="等级信息"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/dialog_music_type"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/primary"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/dialog_music_combo_status"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/primary"
|
||||||
|
|
||||||
|
android:text="连击状态"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/dialog_music_play_count"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/primary"
|
||||||
|
|
||||||
|
android:text="播放次数"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
16
app/src/main/res/layout/search_dialog.xml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?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="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
<TextView android:layout_width="wrap_content" android:layout_height="match_parent" android:text="搜索歌曲" android:textColor="@color/primary"/>
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/search_input"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/primary"
|
||||||
|
android:hint="输入歌曲名称"
|
||||||
|
android:inputType="text" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -6,15 +6,15 @@
|
|||||||
<group android:checkableBehavior="single">
|
<group android:checkableBehavior="single">
|
||||||
<item
|
<item
|
||||||
android:id="@+id/nav_home"
|
android:id="@+id/nav_home"
|
||||||
android:icon="@drawable/ic_menu_camera"
|
|
||||||
android:title="@string/menu_home"/>
|
android:title="@string/menu_home"/>
|
||||||
<item
|
<item
|
||||||
android:id="@+id/nav_gallery"
|
android:id="@+id/nav_gallery"
|
||||||
android:icon="@drawable/ic_menu_gallery"
|
|
||||||
android:title="@string/menu_gallery"/>
|
android:title="@string/menu_gallery"/>
|
||||||
|
<item
|
||||||
|
android:id="@+id/nav_music"
|
||||||
|
android:title="@string/menu_music"/>
|
||||||
<item
|
<item
|
||||||
android:id="@+id/nav_slideshow"
|
android:id="@+id/nav_slideshow"
|
||||||
android:icon="@drawable/ic_menu_slideshow"
|
|
||||||
android:title="@string/menu_slideshow"/>
|
android:title="@string/menu_slideshow"/>
|
||||||
</group>
|
</group>
|
||||||
</menu>
|
</menu>
|
||||||
@@ -5,4 +5,12 @@
|
|||||||
android:title="@string/action_settings"
|
android:title="@string/action_settings"
|
||||||
android:orderInCategory="100"
|
android:orderInCategory="100"
|
||||||
app:showAsAction="never"/>
|
app:showAsAction="never"/>
|
||||||
|
<item android:id="@+id/action_paika"
|
||||||
|
android:title="排卡"
|
||||||
|
android:orderInCategory="100"
|
||||||
|
app:showAsAction="never"/>
|
||||||
|
<item android:id="@+id/action_update"
|
||||||
|
android:title="上传成绩"
|
||||||
|
android:orderInCategory="100"
|
||||||
|
app:showAsAction="never"/>
|
||||||
</menu>
|
</menu>
|
||||||
8
app/src/main/res/menu/main_activity_vpnactions.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
<item android:id="@+id/menu_item_switch"
|
||||||
|
android:title="@string/menu_item_switch"
|
||||||
|
app:showAsAction="always"
|
||||||
|
app:actionViewClass="androidx.appcompat.widget.SwitchCompat"/>
|
||||||
|
|
||||||
|
</menu>
|
||||||
@@ -16,7 +16,11 @@
|
|||||||
android:name="org.astral.findmaimaiultra.ui.gallery.GalleryFragment"
|
android:name="org.astral.findmaimaiultra.ui.gallery.GalleryFragment"
|
||||||
android:label="@string/menu_gallery"
|
android:label="@string/menu_gallery"
|
||||||
tools:layout="@layout/fragment_gallery"/>
|
tools:layout="@layout/fragment_gallery"/>
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/nav_music"
|
||||||
|
android:name="org.astral.findmaimaiultra.ui.music.MusicFragment"
|
||||||
|
android:label="@string/menu_music"
|
||||||
|
tools:layout="@layout/fragment_gallery"/>
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/nav_slideshow"
|
android:id="@+id/nav_slideshow"
|
||||||
android:name="org.astral.findmaimaiultra.ui.slideshow.SlideshowFragment"
|
android:name="org.astral.findmaimaiultra.ui.slideshow.SlideshowFragment"
|
||||||
|
|||||||
@@ -33,7 +33,8 @@
|
|||||||
<string name="navigation_drawer_open">Open navigation drawer</string>
|
<string name="navigation_drawer_open">Open navigation drawer</string>
|
||||||
<string name="navigation_drawer_close">Close navigation drawer</string>
|
<string name="navigation_drawer_close">Close navigation drawer</string>
|
||||||
<string name="menu_home">主页</string>
|
<string name="menu_home">主页</string>
|
||||||
<string name="menu_gallery">歌曲成绩</string>
|
<string name="menu_gallery">地图</string>
|
||||||
|
<string name="menu_music">歌曲成绩</string>
|
||||||
<string name="menu_slideshow">设置</string>
|
<string name="menu_slideshow">设置</string>
|
||||||
<string name="nav_header_title">FindMaimaiDX</string>
|
<string name="nav_header_title">FindMaimaiDX</string>
|
||||||
<string name="nav_header_subtitle">Reisa</string>
|
<string name="nav_header_subtitle">Reisa</string>
|
||||||
|
|||||||
@@ -4,4 +4,13 @@
|
|||||||
<style name="BorderStyle">
|
<style name="BorderStyle">
|
||||||
<item name="android:background">@drawable/border</item>
|
<item name="android:background">@drawable/border</item>
|
||||||
</style>
|
</style>
|
||||||
|
<style name="CustomDialogStyle" parent="Theme.MaterialComponents.Light.Dialog">
|
||||||
|
<item name="shapeAppearance">@style/ShapeAppearance.App.Dialog</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="ShapeAppearance.App.Dialog" parent="ShapeAppearance.MaterialComponents.MediumComponent">
|
||||||
|
<item name="cornerFamily">rounded</item>
|
||||||
|
<item name="cornerSize">16dp</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -19,4 +19,18 @@
|
|||||||
</style>
|
</style>
|
||||||
<style name="Theme.FindMaimaiUltra.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar"/>
|
<style name="Theme.FindMaimaiUltra.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar"/>
|
||||||
<style name="Theme.FindMaimaiUltra.PopupOverlay" parent="ThemeOverlay.AppCompat.Light"/>
|
<style name="Theme.FindMaimaiUltra.PopupOverlay" parent="ThemeOverlay.AppCompat.Light"/>
|
||||||
|
|
||||||
|
<style name="NewTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
|
||||||
|
<!-- Primary brand color. -->
|
||||||
|
<item name="colorPrimary">@color/purple_500</item>
|
||||||
|
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||||
|
<item name="colorOnPrimary">@color/white</item>
|
||||||
|
<!-- Secondary brand color. -->
|
||||||
|
<item name="colorSecondary">@color/teal_200</item>
|
||||||
|
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||||
|
<item name="colorOnSecondary">@color/black</item>
|
||||||
|
<!-- Status bar color. -->
|
||||||
|
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||