Ultra Commit

This commit is contained in:
2025-03-25 22:25:14 +08:00
parent 8443a94b73
commit f85bc1c2a6
75 changed files with 5720 additions and 42 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

1
.idea/gradle.xml generated
View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>

1
.idea/misc.xml generated
View File

@@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration">

View File

@@ -4,14 +4,14 @@ plugins {
android {
namespace 'org.astral.findmaimaiultra'
compileSdk 33
compileSdk 34
defaultConfig {
applicationId "org.astral.findmaimaiultra"
minSdk 30
targetSdk 33
targetSdk 34
versionCode 1
versionName "1.0"
versionName "1.6.0 Ultra"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -72,4 +72,6 @@ dependencies {
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
implementation 'jp.wasabeef:glide-transformations:4.3.0'
}

View File

@@ -36,6 +36,10 @@
android:networkSecurityConfig="@xml/network_security_config"
android:enableOnBackInvokedCallback="true"
android:theme="@style/Theme.FindMaimaiUltra">
<meta-data
android:name="com.baidu.lbsapi.API_KEY"
android:value="lzzpL36kTbcmfQvhWDJOZwa3glQlYBbm"/>
<activity
android:name=".ui.MainActivity"
android:exported="true"
@@ -59,6 +63,20 @@
android:label="@string/app_name"
android:theme="@style/Theme.FindMaimaiUltra.NoActionBar">
</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>
</manifest>

View File

@@ -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);
}
}
}

View File

@@ -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 +
'}';
}
}

View File

@@ -3,20 +3,74 @@ package org.astral.findmaimaiultra.been.faker;
public class MusicRating {
private int musicId;
private String musicName;
private int level;
private double level_info;
private int romVersion;
private int achievement;
private int rating;
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() {
return type;
public int getPlayCount() {
return playCount;
}
public void setType(String type) {
this.type = type;
public void setPlayCount(int playCount) {
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() {
@@ -27,6 +81,14 @@ public class MusicRating {
this.musicName = musicName;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public double getLevel_info() {
return level_info;
}
@@ -72,6 +134,27 @@ public class MusicRating {
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) {
this.achievement = achievement;
}

View File

@@ -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 +
'}';
}
}

View File

@@ -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;
}
}

View File

@@ -1,8 +1,11 @@
package org.astral.findmaimaiultra.ui;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
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.navigation.NavigationView;
import androidx.navigation.NavController;
@@ -27,19 +30,12 @@ public class MainActivity extends AppCompatActivity {
setContentView(binding.getRoot());
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;
NavigationView navigationView = binding.navView;
// Passing each menu ID as a set of Ids because each
// menu should be considered as top level destinations.
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)
.build();
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) {
// Inflate the menu; this adds items to the action bar if it is present.
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

View File

@@ -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) {
}
});
}
}

View File

@@ -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();
}
}

View File

@@ -253,11 +253,12 @@ public class HomeFragment extends Fragment {
// 设置 Toolbar 标题
String navHomeLabel = getString(R.string.menu_home);
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.addToMap("places", new Gson().toJson(a));
sharedViewModel.setPlacelist(new ArrayList<>(a));
// 通知适配器数据已更改
adapter.notifyDataSetChanged();
}
@@ -306,7 +307,6 @@ public class HomeFragment extends Fragment {
public void onLocationChanged(@NonNull Location location) {
Log.d("Location", "onLocationChanged");
if (flag) {
Toast.makeText(context, "定位成功", Toast.LENGTH_SHORT).show();
// 调用高德地图 API 进行逆地理编码
reverseGeocode(location.getLatitude(), location.getLongitude());
}
@@ -392,12 +392,17 @@ public class HomeFragment extends Fragment {
String province = address.getAdminArea();
String city = address.getLocality();
// 更新 UI
requireActivity().runOnUiThread(() -> {
tot = detail;
this.province = province;
this.city = city;
extracted();
});
try {
requireActivity().runOnUiThread(() -> {
tot = detail;
this.province = province;
this.city = city;
extracted();
});
}catch (Exception e) {
}
} else {
Log.d("Location", "Android 自带 Geocoder 获取地址失败");
setDefaultLocation(); // 设置默认位置

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,9 @@
package org.astral.findmaimaiultra.utill.updater.crawler;
public interface Callback {
void onResponse(Object result);
default void onError(Exception error) {
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}
}

View File

@@ -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) {
}
}
}

View File

@@ -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;
}
}

View File

@@ -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>");
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,5 @@
package org.astral.findmaimaiultra.utill.updater.vpn.core;
public class Constant {
public static final String TAG = "VpnProxy";
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}
}
}

View File

@@ -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();
}
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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><36>Ӻ<EFBFBD>һ<EFBFBD>ֽڵ<D6BD><38><CEBB>14λ<34><CEBB>ֵ<EFBFBD><D6B5>
int pointer = buffer.get() & 0xFF;// <20><>
pointer |= (len & 0x3F) << 8;// <20><>
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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,7 @@
package org.astral.findmaimaiultra.utill.updater.vpn.tunnel;
import java.net.InetSocketAddress;
public abstract class Config {
public InetSocketAddress ServerAddress;
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View 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>

View 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>

View File

@@ -23,13 +23,4 @@
<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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -6,15 +6,15 @@
<group android:checkableBehavior="single">
<item
android:id="@+id/nav_home"
android:icon="@drawable/ic_menu_camera"
android:title="@string/menu_home"/>
<item
android:id="@+id/nav_gallery"
android:icon="@drawable/ic_menu_gallery"
android:title="@string/menu_gallery"/>
<item
android:id="@+id/nav_music"
android:title="@string/menu_music"/>
<item
android:id="@+id/nav_slideshow"
android:icon="@drawable/ic_menu_slideshow"
android:title="@string/menu_slideshow"/>
</group>
</menu>

View File

@@ -5,4 +5,12 @@
android:title="@string/action_settings"
android:orderInCategory="100"
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>

View 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>

View File

@@ -16,7 +16,11 @@
android:name="org.astral.findmaimaiultra.ui.gallery.GalleryFragment"
android:label="@string/menu_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
android:id="@+id/nav_slideshow"
android:name="org.astral.findmaimaiultra.ui.slideshow.SlideshowFragment"

View File

@@ -33,7 +33,8 @@
<string name="navigation_drawer_open">Open navigation drawer</string>
<string name="navigation_drawer_close">Close navigation drawer</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="nav_header_title">FindMaimaiDX</string>
<string name="nav_header_subtitle">Reisa</string>

View File

@@ -4,4 +4,13 @@
<style name="BorderStyle">
<item name="android:background">@drawable/border</item>
</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>

View File

@@ -19,4 +19,18 @@
</style>
<style name="Theme.FindMaimaiUltra.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar"/>
<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>