增加天气显示

This commit is contained in:
2026-03-06 09:50:58 +08:00
parent 962a022bf9
commit 0f2adad060
30 changed files with 5567 additions and 64 deletions

44
app/CMakeLists.txt Normal file
View File

@@ -0,0 +1,44 @@
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.4.1)
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
add_library( # Sets the name of the library.
ttstd
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/jni/ttstd.cpp)
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log)
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
ttstd
# Links the target library to the log library
# included in the NDK.
${log-lib})

View File

@@ -63,9 +63,6 @@ android {
}
}
buildConfigField "String", "QweatherId", "\"${qweatherConfigs.credential.id}\""
buildConfigField "String", "QweatherKey", "\"${qweatherConfigs.credential.key}\""
manifestPlaceholders = [
JPUSH_PKGNAME : applicationId,
JPUSH_APPKEY : "d779178d9900d4fb5d633678", //JPush 上注册的包名对应的 Appkey.
@@ -84,6 +81,12 @@ android {
]
}
externalNativeBuild {
cmake {
path file('CMakeLists.txt')
}
}
signingConfigs {
keypub {
storeFile file(rootProject.ext.signingConfigs.keypub.storeFile)
@@ -177,7 +180,8 @@ ext {
dependencies {
// implementation fileTree(dir: 'libs', include: ['*.jar'])
//Android 4.4+
implementation files('libs/QWeather_Public_Android_V4.20.jar')
implementation files('libs/QWeather_Public_Android_V5.1.2.jar')
implementation 'net.i2p.crypto:eddsa:0.3.0'
implementation project(path: ':niceimageview')
implementation project(path: ':iconloader')
@@ -311,6 +315,7 @@ dependencies {
implementation 'com.gitee.zackratos:UltimateBarX:0.8.0'
//指示器
implementation 'com.github.hackware1993:MagicIndicator:1.7.0'
implementation 'com.opencsv:opencsv:5.5.2'
// 吐司框架https://github.com/getActivity/Toaster
implementation 'com.github.getActivity:Toaster:12.6'
@@ -320,5 +325,4 @@ dependencies {
//https://github.com/JessYanCoding/AndroidAutoSize
implementation 'me.jessyan:autosize:1.2.1'
}

Binary file not shown.

View File

@@ -67,6 +67,10 @@
android:name=".activity.app.AppListActivity"
android:launchMode="singleTask"
android:screenOrientation="portrait" />
<activity android:name=".activity.weather.main.WeatherMainActivity"
android:launchMode="singleTask"
android:theme="@style/AppThemeBlack"
android:screenOrientation="portrait" />
<service
android:name=".service.DialerAccessibilityService"
@@ -105,10 +109,11 @@
android:resource="@xml/file_paths" />
</provider>
<!--baidu map-->
<service
android:name="com.baidu.location.f"
android:enabled="true"
android:process=":remote" />
android:process=":location" />
<!--(jpush|jad)_config_startjpush和jad公用的组件-->
<!-- 可配置android:process参数将PushService放在其他进程中 -->

File diff suppressed because it is too large Load Diff

View File

@@ -33,7 +33,7 @@ import me.jessyan.autosize.AutoSize;
public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBinding> {
private static final String TAG = "MainActivity";
protected MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE);
private MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE);
private FragmentManager mFragmentManager = getSupportFragmentManager();
// private BaseFragmentPagerAdapter mBaseFragmentPagerAdapter;

View File

@@ -0,0 +1,105 @@
package com.ttstd.dialer.activity.weather.main;
import android.util.Log;
import androidx.lifecycle.Observer;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.baidu.location.BDLocation;
import com.jeremyliao.liveeventbus.LiveEventBus;
import com.qweather.sdk.response.weather.WeatherDaily;
import com.qweather.sdk.response.weather.WeatherNow;
import com.tencent.mmkv.MMKV;
import com.ttstd.dialer.R;
import com.ttstd.dialer.adapter.WeatherAdapter;
import com.ttstd.dialer.base.mvvm.BaseMvvmActivity;
import com.ttstd.dialer.config.CommonConfig;
import com.ttstd.dialer.databinding.ActivityWeatherMainBinding;
import com.ttstd.dialer.utils.DateUtil;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
public class WeatherMainActivity extends BaseMvvmActivity<WeatherMainViewModel, ActivityWeatherMainBinding> {
private static final String TAG = "WeatherMainActivity";
private MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE);
private WeatherAdapter mWeatherAdapter;
// @Override
// public boolean setfitWindow() {
// return true;
// }
@Override
protected int getLayoutId() {
return R.layout.activity_weather_main;
}
@Override
protected void initDataBinding() {
mViewModel.setContext(this);
mViewModel.setVDBinding(mViewDataBinding);
mViewModel.setLifecycle(getLifecycleSubject());
mViewDataBinding.setClick(new BtnClick());
}
@Override
protected void initView() {
mWeatherAdapter = new WeatherAdapter();
mViewDataBinding.recyclerView.setAdapter(mWeatherAdapter);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(WeatherMainActivity.this, LinearLayoutManager.VERTICAL, false);
mViewDataBinding.recyclerView.setLayoutManager(linearLayoutManager);
}
@Override
protected void initData() {
Log.e(TAG, "initData: ");
mViewModel.mWeatherNowData.observe(this, new Observer<WeatherNow>() {
@Override
public void onChanged(WeatherNow weatherNow) {
mViewDataBinding.setWeatherNow(weatherNow);
}
});
mViewModel.mWeatherDailyListData.observe(this, new Observer<List<WeatherDaily>>() {
@Override
public void onChanged(List<WeatherDaily> weatherDailies) {
Optional<WeatherDaily> dailyOptional = weatherDailies.stream().filter(new Predicate<WeatherDaily>() {
@Override
public boolean test(WeatherDaily weatherDaily) {
return DateUtil.isTodayAll(weatherDaily.getFxDate());
}
}).findAny();
if (dailyOptional.isPresent()) {
WeatherDaily weatherDaily = dailyOptional.get();
mViewDataBinding.tvTempRange.setText("最高" + weatherDaily.getTempMax() + "°" + "\t" + "最低" + weatherDaily.getTempMin() + "°");
} else {
}
mWeatherAdapter.setWeatherDailyList(weatherDailies);
}
});
boolean manually = mMMKV.decodeBool(CommonConfig.MANUALLY_SELECT_LOCATION, false);
if (manually) {
} else {
LiveEventBus.get(CommonConfig.LOCATION_CHANGED_KEY, BDLocation.class)
.observeSticky(this, new Observer<BDLocation>() {
@Override
public void onChanged(BDLocation bdLocation) {
mViewDataBinding.tvLocation.setText(bdLocation.getDistrict());
mViewModel.getWeatherNow(bdLocation.getAdCode());
mViewModel.getWeather10D(bdLocation.getAdCode());
}
});
}
}
public class BtnClick {
}
}

View File

@@ -0,0 +1,75 @@
package com.ttstd.dialer.activity.weather.main;
import android.util.Log;
import androidx.lifecycle.MutableLiveData;
import com.qweather.sdk.Callback;
import com.qweather.sdk.response.error.ErrorResponse;
import com.qweather.sdk.response.weather.WeatherDaily;
import com.qweather.sdk.response.weather.WeatherDailyResponse;
import com.qweather.sdk.response.weather.WeatherNow;
import com.qweather.sdk.response.weather.WeatherNowResponse;
import com.trello.rxlifecycle4.android.ActivityEvent;
import com.ttstd.dialer.base.mvvm.BaseViewModel;
import com.ttstd.dialer.databinding.ActivityWeatherMainBinding;
import com.ttstd.dialer.gson.GsonUtils;
import com.ttstd.dialer.manager.WeatherManager;
import java.util.List;
public class WeatherMainViewModel extends BaseViewModel<ActivityWeatherMainBinding, ActivityEvent> {
private static final String TAG = "WeatherMainViewModel";
public MutableLiveData<WeatherNow> mWeatherNowData = new MutableLiveData<>();
public void getWeatherNow(String adCode) {
Log.e(TAG, "getWeatherNow: " + adCode);
WeatherManager.getInstance().getWeatherNow(adCode, new Callback<WeatherNowResponse>() {
@Override
public void onSuccess(WeatherNowResponse response) {
Log.e("getWeatherNow", "onSuccess: " + response);
WeatherNow weatherNow = response.getNow();
Log.e("getWeatherNow", "onSuccess: " + weatherNow);
mWeatherNowData.postValue(weatherNow);
}
@Override
public void onFailure(ErrorResponse errorResponse) {
Log.e("getWeatherNow", "onFailure: " + errorResponse.getError().getStatus());
Log.e("getWeatherNow", "onFailure: " + errorResponse.getError().getDetail());
}
@Override
public void onException(Throwable e) {
Log.e("getWeatherNow", "onException: " + e.getMessage());
}
});
}
public MutableLiveData<List<WeatherDaily>> mWeatherDailyListData = new MutableLiveData<>();
public void getWeather10D(String adCode) {
Log.e(TAG, "getWeather10D: " + adCode);
WeatherManager.getInstance().getWeather10D(adCode, new Callback<WeatherDailyResponse>() {
@Override
public void onSuccess(WeatherDailyResponse weatherDailyResponse) {
Log.e("getWeather10D", "onSuccess: ");
List<WeatherDaily> weatherDailyList = weatherDailyResponse.getDaily();
Log.e("getWeather10D", "onSuccess: "+ GsonUtils.toJSONString(weatherDailyList));
mWeatherDailyListData.postValue(weatherDailyList);
}
@Override
public void onFailure(ErrorResponse errorResponse) {
Log.e("getWeather10D", "onFailure: ");
}
@Override
public void onException(Throwable throwable) {
Log.e("getWeather10D", "onException: " + throwable.getMessage());
}
});
}
}

View File

@@ -0,0 +1,77 @@
package com.ttstd.dialer.adapter;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.fragment.app.FragmentActivity;
import androidx.recyclerview.widget.RecyclerView;
import com.qweather.sdk.response.weather.WeatherDaily;
import com.tencent.mmkv.MMKV;
import com.ttstd.dialer.R;
import com.ttstd.dialer.config.CommonConfig;
import com.ttstd.dialer.utils.DateUtil;
import java.util.List;
import java.util.Locale;
public class WeatherAdapter extends RecyclerView.Adapter<WeatherAdapter.WeatherHolder> {
private static final String TAG = "WeatherAdapter";
private MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE);
private FragmentActivity mContext;
private List<WeatherDaily> mWeatherDailyList;
public void setWeatherDailyList(List<WeatherDaily> weatherDailyList) {
mWeatherDailyList = weatherDailyList;
notifyDataSetChanged();
}
@Override
public WeatherHolder onCreateViewHolder(ViewGroup parent, int viewType) {
mContext = (FragmentActivity) parent.getContext();
return new WeatherHolder(LayoutInflater.from(mContext).inflate(R.layout.item_weather, parent, false));
}
@Override
public void onBindViewHolder(WeatherHolder holder, int position) {
WeatherDaily weatherDaily = mWeatherDailyList.get(position);
String date = weatherDaily.getFxDate();
if (DateUtil.isTodayAll(date)) {
holder.tv_date.setText("今天");
} else {
String week = DateUtil.convertToChineseWeekdayAll(date);
holder.tv_date.setText(week);
}
String tempMin = weatherDaily.getTempMin();
holder.tv_temp_min.setText(tempMin + "°");
String tempMax = weatherDaily.getTempMax();
holder.tv_temp_max.setText(tempMax + "°");
String iconDay = weatherDaily.getIconDay();
}
@Override
public int getItemCount() {
return mWeatherDailyList == null ? 0 : mWeatherDailyList.size();
}
public class WeatherHolder extends RecyclerView.ViewHolder {
TextView tv_date, tv_temp_min, tv_temp_max;
ImageView iv_icon;
public WeatherHolder(View itemView) {
super(itemView);
tv_date = itemView.findViewById(R.id.tv_date);
tv_temp_min = itemView.findViewById(R.id.tv_temp_min);
tv_temp_max = itemView.findViewById(R.id.tv_temp_max);
iv_icon = itemView.findViewById(R.id.iv_icon);
}
}
}

View File

@@ -11,11 +11,13 @@ import android.util.Log;
import com.alibaba.android.arouter.launcher.ARouter;
import com.arialyy.aria.core.Aria;
import com.hjq.toast.Toaster;
import com.qweather.sdk.view.HeConfig;
import com.tencent.bugly.crashreport.CrashReport;
import com.tencent.mmkv.MMKV;
import com.ttstd.dialer.BuildConfig;
import com.ttstd.dialer.manager.AppManager;
import com.ttstd.dialer.manager.MapManager;
import com.ttstd.dialer.manager.WeatherManager;
import com.ttstd.dialer.utils.Logger;
import com.ttstd.dialer.utils.SystemUtils;
import com.ttstd.iconloader.IconCacheManager;
@@ -55,6 +57,9 @@ public class BaseApplication extends Application {
private void init() {
Log.e(TAG, "init: ");
if (SystemUtils.isMainProcessName(this, android.os.Process.myPid())) {
Logger.initialize(this, BuildConfig.DEBUG);
Logger.setLogLevel(Logger.LogLevel.DEBUG); // 开发阶段记录所有日志
String rootDir = MMKV.initialize(this);
Log.e(TAG, "mmkv root: " + rootDir);
@@ -71,17 +76,13 @@ public class BaseApplication extends Application {
return;
}
JPushInterface.init(this);
JPushInterface.setAlias(this, );
JPushInterface.setAlias(this, 0, SystemUtils.getSerial());
// 调整点二App用户同意了隐私政策授权并且开发者确定要开启推送服务后调用
// JCore 5.0.4+会自动处理授权状态可不需要显式设置true
JCollectionAuth.setAuth(context, true);
/*jpush end*/
HeConfig.init(BuildConfig.QweatherId, BuildConfig.QweatherKey);
//切换至免费订阅
HeConfig.switchToDevService();
if (BuildConfig.DEBUG) { // 这两行必须写在init之前否则这些配置在init过程中将无效
ARouter.openLog(); // 打印日志
ARouter.openDebug(); // 开启调试模式(如果在InstantRun模式下运行必须开启调试模式线上版本需要关闭,否则有安全风险)
@@ -98,6 +99,9 @@ public class BaseApplication extends Application {
xcrash.XCrash.init(this);
AppManager.init(this);
MapManager.init(this);
MapManager.getInstance().initMap();
WeatherManager.init(this);
IconCacheManager.init(this);
}
}

View File

@@ -0,0 +1,158 @@
package com.ttstd.dialer.bean;
import androidx.annotation.NonNull;
import com.google.gson.Gson;
import com.google.gson.JsonParser;
import com.opencsv.bean.CsvBindByName;
import java.io.Serializable;
public class CityInfo implements Serializable {
@CsvBindByName(column = "Location_ID")
String Location_ID;
@CsvBindByName(column = "Location_Name_EN")
String Location_Name_EN;
@CsvBindByName(column = "Location_Name_ZH")
String Location_Name_ZH;
@CsvBindByName(column = "ISO_3166_1")
String ISO_3166_1;
@CsvBindByName(column = "Country_Region_EN")
String Country_Region_EN;
@CsvBindByName(column = "Country_Region_ZH")
String Country_Region_ZH;
@CsvBindByName(column = "Adm1_Name_EN")
String Adm1_Name_EN;
@CsvBindByName(column = "Adm1_Name_ZH")
String Adm1_Name_ZH;
@CsvBindByName(column = "Adm2_Name_EN")
String Adm2_Name_EN;
@CsvBindByName(column = "Adm2_Name_ZH")
String Adm2_Name_ZH;
@CsvBindByName(column = "Timezone")
String Timezone;
@CsvBindByName(column = "Latitude")
String Latitude;
@CsvBindByName(column = "Longitude")
String Longitude;
@CsvBindByName(column = "AD_code")
String AD_code;
public String getLocation_ID() {
return Location_ID;
}
public void setLocation_ID(String location_ID) {
Location_ID = location_ID;
}
public String getLocation_Name_EN() {
return Location_Name_EN;
}
public void setLocation_Name_EN(String location_Name_EN) {
Location_Name_EN = location_Name_EN;
}
public String getLocation_Name_ZH() {
return Location_Name_ZH;
}
public void setLocation_Name_ZH(String location_Name_ZH) {
Location_Name_ZH = location_Name_ZH;
}
public String getISO_3166_1() {
return ISO_3166_1;
}
public void setISO_3166_1(String ISO_3166_1) {
this.ISO_3166_1 = ISO_3166_1;
}
public String getCountry_Region_EN() {
return Country_Region_EN;
}
public void setCountry_Region_EN(String country_Region_EN) {
Country_Region_EN = country_Region_EN;
}
public String getCountry_Region_ZH() {
return Country_Region_ZH;
}
public void setCountry_Region_ZH(String country_Region_ZH) {
Country_Region_ZH = country_Region_ZH;
}
public String getAdm1_Name_EN() {
return Adm1_Name_EN;
}
public void setAdm1_Name_EN(String adm1_Name_EN) {
Adm1_Name_EN = adm1_Name_EN;
}
public String getAdm1_Name_ZH() {
return Adm1_Name_ZH;
}
public void setAdm1_Name_ZH(String adm1_Name_ZH) {
Adm1_Name_ZH = adm1_Name_ZH;
}
public String getAdm2_Name_EN() {
return Adm2_Name_EN;
}
public void setAdm2_Name_EN(String adm2_Name_EN) {
Adm2_Name_EN = adm2_Name_EN;
}
public String getAdm2_Name_ZH() {
return Adm2_Name_ZH;
}
public void setAdm2_Name_ZH(String adm2_Name_ZH) {
Adm2_Name_ZH = adm2_Name_ZH;
}
public String getTimezone() {
return Timezone;
}
public void setTimezone(String timezone) {
Timezone = timezone;
}
public String getLatitude() {
return Latitude;
}
public void setLatitude(String latitude) {
Latitude = latitude;
}
public String getLongitude() {
return Longitude;
}
public void setLongitude(String longitude) {
Longitude = longitude;
}
public String getAD_code() {
return AD_code;
}
public void setAD_code(String AD_code) {
this.AD_code = AD_code;
}
@NonNull
@Override
public String toString() {
return JsonParser.parseString(new Gson().toJson(this)).getAsJsonObject().toString();
}
}

View File

@@ -5,4 +5,32 @@ public class CommonConfig {
public static final String CONTACT_HOME_PAGE = "contact_home_page_key";
public static final String MANUALLY_SELECT_LOCATION = "manually_select_location_key";
/*地址改变时发送*/
public static final String LOCATION_CHANGED_KEY = "map_location_changed";
public static final String LOCATION_ID_CHANGED_KEY = "map_Location_ID_changed";
/*当前定位地址*/
public static final String CURRENT_LOCATION_MAP_ADDRESS_KEY = "current_map_location_map_address";
public static final String CURRENT_LOCATION_MAP_LOCATION_DESCRIBE_KEY = "current_map_location_map_location_describe";
/*省*/
public static final String CURRENT_LOCATION_PROVINCE_KEY = "current_map_location_province";
/*市*/
public static final String CURRENT_LOCATION_CITY_KEY = "current_map_location_city";
/*区*/
public static final String CURRENT_LOCATION_DISTRICT_KEY = "current_map_location_district";
public static final String CURRENT_LOCATION_TOWN_KEY = "current_map_location_town";
public static final String CURRENT_LOCATION_STREET_KEY = "current_map_location_street";
/*当前定位经度*/
public static final String CURRENT_LOCATION_LONGITUDE_KEY = "current_map_location_longitude";
/*当前定位纬度*/
public static final String CURRENT_LOCATION_LATITUDE_KEY = "current_map_location_latitude";
/*当前ad_code*/
public static final String CURRENT_LOCATION_AD_CODE_KEY = "current_map_location_ad_code";
/*当前LocationID*/
@Deprecated
public static final String CURRENT_LOCATION_ID_KEY = "current_map_location_ID";
}

View File

@@ -6,7 +6,6 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
import android.util.Log;
@@ -17,10 +16,11 @@ import androidx.fragment.app.Fragment;
import com.hjq.toast.Toaster;
import com.ttstd.dialer.R;
import com.ttstd.dialer.activity.contact.list.ContactListActivity;
import com.ttstd.dialer.activity.weather.main.WeatherMainActivity;
import com.ttstd.dialer.base.mvvm.fragment.BaseMvvmFragment;
import com.ttstd.dialer.databinding.FragmentHomeBinding;
import com.ttstd.dialer.utils.ApkUtils;
import com.ttstd.dialer.utils.DataUtil;
import com.ttstd.dialer.utils.DateUtil;
import com.ttstd.dialer.utils.LunarCalendarFestivalUtils;
/**
@@ -166,7 +166,7 @@ public class HomeFragment extends BaseMvvmFragment<HomeViewModel, FragmentHomeBi
private void setTime() {
if (isAdded()) {
mViewDataBinding.tvTime.setText(DataUtil.formatDateHour());
mViewDataBinding.tvTime.setText(DateUtil.formatDateHour());
// mViewDataBinding.tvDate.setText(DataUtil.formatDateDay());
// mViewDataBinding.tvWeek.setText(TimeUtils.getWeek());
// mViewDataBinding.tvLunar.setText(mFestivalUtils.getLunarCalendar());
@@ -220,6 +220,9 @@ public class HomeFragment extends BaseMvvmFragment<HomeViewModel, FragmentHomeBi
}
}
public void openWeather(View view) {
startActivity(new Intent(mContext, WeatherMainActivity.class));
}
}

View File

@@ -6,4 +6,5 @@ import com.ttstd.dialer.databinding.FragmentHomeBinding;
public class HomeViewModel extends BaseViewModel<FragmentHomeBinding, FragmentEvent> {
}

View File

@@ -0,0 +1,102 @@
package com.ttstd.dialer.manager;
import android.content.Context;
import android.util.Log;
import com.opencsv.bean.CsvToBean;
import com.opencsv.bean.CsvToBeanBuilder;
import com.opencsv.bean.HeaderColumnNameMappingStrategy;
import com.ttstd.dialer.bean.CityInfo;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
public class CsvDeserializer {
private static final String TAG = "CsvDeserializer";
/**
* 从assets文件夹中的CSV文件反序列化为对象列表
*
* @param context 上下文
* @param fileName assets中的CSV文件名
* @return 对象列表
*/
public static List<CityInfo> deserializeFromAssets(Context context, String fileName) {
InputStream inputStream = null;
Reader reader = null;
try {
long time = System.currentTimeMillis();
// 从assets获取CSV文件输入流
inputStream = context.getAssets().open(fileName);
reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
// 创建映射策略使用CSV头部列名映射到对象字段
HeaderColumnNameMappingStrategy<CityInfo> strategy = new HeaderColumnNameMappingStrategy<>();
strategy.setType(CityInfo.class);
// 创建CsvToBean并进行转换
CsvToBean<CityInfo> csvToBean = new CsvToBeanBuilder<CityInfo>(reader)
.withMappingStrategy(strategy)
.withIgnoreLeadingWhiteSpace(true)
.withIgnoreEmptyLine(true) // 忽略空行
.withThrowExceptions(false) // 不抛出异常,便于调试
.build();
Log.e(TAG, "deserializeFromAssets: finish " + (System.currentTimeMillis() - time) + "ms");
// 转换并返回结果列表
return csvToBean.parse();
} catch (IOException e) {
Log.e(TAG, "Error reading CSV file: " + e.getMessage());
e.printStackTrace();
} catch (Exception e) {
Log.e(TAG, "Error during CSV deserialization: " + e.getMessage());
e.printStackTrace();
} finally {
// 确保关闭流,避免资源泄漏
try {
if (reader != null) {
reader.close();
}
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
Log.w(TAG, "Failed to close stream: " + e.getMessage());
}
}
return new ArrayList<>();
}
/**
* 从输入流反序列化为对象列表
*
* @param inputStream CSV文件输入流
* @return 对象列表
*/
public static List<CityInfo> deserializeFromStream(InputStream inputStream) {
try {
Reader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
HeaderColumnNameMappingStrategy<CityInfo> strategy = new HeaderColumnNameMappingStrategy<>();
strategy.setType(CityInfo.class);
CsvToBean<CityInfo> csvToBean = new CsvToBeanBuilder<CityInfo>(reader)
.withMappingStrategy(strategy)
.withIgnoreLeadingWhiteSpace(true)
.build();
return csvToBean.parse();
} catch (Exception e) {
Log.e(TAG, "Error during CSV deserialization: " + e.getMessage());
e.printStackTrace();
}
return null;
}
}

View File

@@ -0,0 +1,363 @@
package com.ttstd.dialer.manager;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
import androidx.core.app.ActivityCompat;
import com.baidu.location.BDAbstractLocationListener;
import com.baidu.location.BDLocation;
import com.baidu.location.LocationClient;
import com.baidu.location.LocationClientOption;
import com.jeremyliao.liveeventbus.LiveEventBus;
import com.tencent.mmkv.MMKV;
import com.ttstd.dialer.config.CommonConfig;
import com.ttstd.dialer.gson.GsonUtils;
import com.ttstd.dialer.utils.Logger;
import com.ttstd.dialer.utils.SystemUtils;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
public class MapManager {
private static final String TAG = "MapManager";
private static final String[] REQUIRED_PERMISSIONS = {
android.Manifest.permission.ACCESS_FINE_LOCATION,
android.Manifest.permission.ACCESS_COARSE_LOCATION
};
// 线程安全的单例模式(双重检查锁)
@SuppressLint("StaticFieldLeak")
private static volatile MapManager sInstance;
private final Context mContext; // 使用Application Context避免内存泄漏
private final MMKV mMMKV;
private LocationClient mLocationClient;
private LocationClientOption mOption;
// 线程安全的回调列表
private final List<OnLocationListener> mListeners = new CopyOnWriteArrayList<>();
// 定位结果回调接口
public interface OnLocationListener {
void onLocationSuccess(BDLocation location); // 定位成功
void onLocationFailure(String errorMsg); // 定位失败
}
private MapManager(Context context) {
// 使用Application Context避免持有Activity导致内存泄漏
this.mContext = context.getApplicationContext();
this.mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE);
}
// 初始化单例(线程安全)
public static void init(Context context) {
if (context == null) {
throw new RuntimeException("Context cannot be null");
}
if (sInstance == null) {
synchronized (MapManager.class) {
if (sInstance == null) {
sInstance = new MapManager(context);
}
}
}
}
public static MapManager getInstance() {
if (sInstance == null) {
throw new IllegalStateException("MapManager must be initialized first with init(Context)");
}
return sInstance;
}
/**
* 初始化定位客户端(包含权限检查)
*/
public void initMap() {
if (!checkLocationPermissions()) {
Logger.e(TAG, "Location permissions are missing");
notifyLocationFailure("缺少定位权限,请先授予权限");
return;
}
try {
// 设置系统定位参数(抽取为独立方法)
setupSystemLocationSettings();
} catch (Exception e) {
Logger.e(TAG, "Failed to setup system location settings", e.getMessage());
notifyLocationFailure("系统定位设置失败:" + e.getMessage());
}
LocationClient.setAgreePrivacy(true);
if (mLocationClient == null) {
try {
mLocationClient = new LocationClient(mContext);
mLocationClient.setLocOption(getDefaultLocationClientOption());
mLocationClient.registerLocationListener(mLocationListener);
// 确保先停止再启动,避免重复启动
if (mLocationClient.isStarted()) {
mLocationClient.stop();
}
mLocationClient.start();
} catch (Exception e) {
e.printStackTrace();
Logger.e(TAG, "initMap: " + e.getMessage());
}
}
}
/**
* 检查定位权限
*/
private boolean checkLocationPermissions() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return true; // 6.0以下默认有权限
}
for (String permission : REQUIRED_PERMISSIONS) {
if (ActivityCompat.checkSelfPermission(mContext, permission)
!= PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
/**
* 配置系统定位参数(抽取重复代码)
*/
private void setupSystemLocationSettings() {
// Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.ASSISTED_GPS_ENABLED, 1);
Settings.Global.putInt(mContext.getContentResolver(), "assisted_gps_enabled", 1);
// 合并定位提供者设置(原代码重复设置会覆盖,改为追加)
String providers = Settings.Secure.getString(mContext.getContentResolver(),
Settings.Secure.LOCATION_PROVIDERS_ALLOWED);
if (TextUtils.isEmpty(providers)) {
providers = "gps,network";
} else {
if (!providers.contains("gps")) providers += ",gps";
if (!providers.contains("network")) providers += ",network";
}
Settings.Secure.putString(mContext.getContentResolver(), Settings.Secure.LOCATION_PROVIDERS_ALLOWED, providers);
}
/**
* 获取定位客户端(懒加载)
*/
public LocationClient getLocationClient() {
if (mLocationClient == null) {
initMap();
}
return mLocationClient;
}
/**
* 配置定位参数(允许外部自定义)
*/
public void setLocationOption(LocationClientOption option) {
if (option != null && mLocationClient != null) {
mOption = option;
mLocationClient.setLocOption(option);
}
}
/**
* 默认定位参数配置(修复重复设置问题)
*/
public LocationClientOption getDefaultLocationClientOption() {
if (mOption == null) {
mOption = new LocationClientOption();
mOption.setCoorType("bd09ll"); // 百度坐标系(配合百度地图使用)
mOption.setScanSpan(0); // 仅定位一次0表示单次定位
mOption.setIsNeedAddress(true); // 需要地址信息
mOption.setIsNeedLocationDescribe(true); // 需要地址描述
mOption.setNeedDeviceDirect(false); // 不需要设备方向
mOption.setLocationNotify(false); // 不频繁输出GPS结果
mOption.setIgnoreKillProcess(true); // 定位服务独立进程
mOption.setIsNeedLocationPoiList(true); // 需要POI结果
mOption.SetIgnoreCacheException(false); // 收集错误信息
mOption.setLocationMode(LocationClientOption.LocationMode.Battery_Saving); // 低功耗模式
mOption.setIsNeedAltitude(false); // 不需要海拔信息
}
return mOption;
}
/**
* 注册定位回调
*/
public void registerLocationListener(OnLocationListener listener) {
if (listener != null && !mListeners.contains(listener)) {
mListeners.add(listener);
}
}
/**
* 反注册定位回调(避免内存泄漏)
*/
public void unregisterLocationListener(OnLocationListener listener) {
if (listener != null) {
mListeners.remove(listener);
}
}
/**
* 通知所有监听器定位成功
*/
private void notifyLocationSuccess(BDLocation location) {
for (OnLocationListener listener : mListeners) {
try {
listener.onLocationSuccess(location);
} catch (Exception e) {
Logger.e(TAG, "Error in onLocationSuccess callback", e.getMessage());
}
}
}
/**
* 通知所有监听器定位失败
*/
private void notifyLocationFailure(String errorMsg) {
for (OnLocationListener listener : mListeners) {
try {
listener.onLocationFailure(errorMsg);
} catch (Exception e) {
Logger.e(TAG, "Error in onLocationFailure callback", e.getMessage());
}
}
}
/**
* 存储定位结果(解耦存储逻辑)
*/
private void saveLocationResult(BDLocation location) {
if (location == null) return;
mMMKV.encode(CommonConfig.CURRENT_LOCATION_MAP_ADDRESS_KEY, location.getAddrStr());
mMMKV.encode(CommonConfig.CURRENT_LOCATION_MAP_LOCATION_DESCRIBE_KEY, location.getLocationDescribe());
mMMKV.encode(CommonConfig.CURRENT_LOCATION_PROVINCE_KEY, location.getProvince());
mMMKV.encode(CommonConfig.CURRENT_LOCATION_CITY_KEY, location.getCity());
mMMKV.encode(CommonConfig.CURRENT_LOCATION_DISTRICT_KEY, location.getDistrict());
mMMKV.encode(CommonConfig.CURRENT_LOCATION_TOWN_KEY, location.getTown());
mMMKV.encode(CommonConfig.CURRENT_LOCATION_STREET_KEY, location.getStreet());
mMMKV.encode(CommonConfig.CURRENT_LOCATION_LONGITUDE_KEY, location.getLongitude());
mMMKV.encode(CommonConfig.CURRENT_LOCATION_LATITUDE_KEY, location.getLatitude());
mMMKV.encode(CommonConfig.CURRENT_LOCATION_AD_CODE_KEY, location.getAdCode());
// mMMKV.encode(CommonConfig.CURRENT_LOCATION_LOCATION_ID_KEY, location.getLocationID());
mMMKV.encode("MapError", "-");
Map<String, Object> params = new LinkedHashMap<>();
params.put("sn", SystemUtils.getSerial());
params.put("address", location.getAddrStr());
params.put("location_describe", location.getLocationDescribe());
params.put("longitude", location.getLongitude());
params.put("latitude", location.getLatitude());
params.put("ad_code", location.getAdCode());
params.put("coor_type", location.getCoorType());
Logger.e(TAG, "saveLocationResult", "location = " + GsonUtils.toJSONString(location));
WeatherManager.getInstance().setAdCode(location.getAdCode());
LiveEventBus.get(CommonConfig.LOCATION_CHANGED_KEY)
.post(location);
}
/**
* 存储定位错误信息
*/
private void saveLocationError(String errorMsg) {
mMMKV.encode("MapError", errorMsg);
}
// 百度定位回调
private final BDAbstractLocationListener mLocationListener = new BDAbstractLocationListener() {
@Override
public void onReceiveLocation(BDLocation location) {
if (location == null) {
String error = "定位结果为空";
Logger.e(TAG, "onReceiveLocation", error);
saveLocationError(error);
notifyLocationFailure(error);
stopLocation();
return;
}
switch (location.getLocType()) {
case BDLocation.TypeGpsLocation:
case BDLocation.TypeNetWorkLocation:
case BDLocation.TypeOffLineLocation:
Logger.e(TAG, "onReceiveLocation", "定位成功: " + location.getAddrStr() + location.getLocationDescribe());
saveLocationResult(location);
notifyLocationSuccess(location);
break;
case BDLocation.TypeServerError:
String serverError = "服务端定位失败,请反馈问题";
Logger.e(TAG, "onReceiveLocation", serverError);
saveLocationError(serverError);
notifyLocationFailure(serverError);
break;
case BDLocation.TypeNetWorkException:
String networkError = "网络异常导致定位失败,请检查网络";
Logger.e(TAG, "onReceiveLocation", networkError);
saveLocationError(networkError);
notifyLocationFailure(networkError);
break;
case BDLocation.TypeCriteriaException:
String criteriaError = "无法获取定位依据,请检查设备状态";
Logger.e(TAG, "onReceiveLocation", criteriaError);
saveLocationError(criteriaError);
notifyLocationFailure(criteriaError);
break;
default:
String unknownError = "未知定位错误: " + location.getLocType();
Logger.e(TAG, "onReceiveLocation", unknownError);
saveLocationError(unknownError);
notifyLocationFailure(unknownError);
break;
}
// 单次定位完成后停止(如果是单次定位模式)
if (mOption != null && mOption.getScanSpan() == 0) {
stopLocation();
}
}
@Override
public void onLocDiagnosticMessage(int locType, int diagnosticType, String diagnosticMessage) {
super.onLocDiagnosticMessage(locType, diagnosticType, diagnosticMessage);
String errorMsg = "定位诊断: " + diagnosticMessage;
Logger.e(TAG, "onLocDiagnosticMessage", errorMsg);
saveLocationError(errorMsg);
notifyLocationFailure(errorMsg);
}
};
/**
* 停止定位并释放资源
*/
public void stopLocation() {
if (mLocationClient != null && mLocationClient.isStarted()) {
mLocationClient.stop();
}
}
/**
* 销毁定位客户端建议在Application退出时调用
*/
public void destroy() {
stopLocation();
if (mLocationClient != null) {
mLocationClient.unRegisterLocationListener(mLocationListener);
mLocationClient = null;
}
mListeners.clear(); // 清除所有回调,避免内存泄漏
}
}

View File

@@ -0,0 +1,171 @@
package com.ttstd.dialer.manager;
import android.annotation.SuppressLint;
import android.content.Context;
import android.text.TextUtils;
import android.util.Log;
import com.jeremyliao.liveeventbus.LiveEventBus;
import com.qweather.sdk.Callback;
import com.qweather.sdk.JWTGenerator;
import com.qweather.sdk.QWeather;
import com.qweather.sdk.basic.Lang;
import com.qweather.sdk.basic.Unit;
import com.qweather.sdk.parameter.weather.WeatherParameter;
import com.qweather.sdk.response.weather.WeatherDailyResponse;
import com.qweather.sdk.response.weather.WeatherNowResponse;
import com.ttstd.dialer.BuildConfig;
import com.ttstd.dialer.bean.CityInfo;
import com.ttstd.dialer.config.CommonConfig;
import com.ttstd.dialer.utils.Logger;
import com.ttstd.dialer.utils.NativeUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.annotations.NonNull;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.ObservableEmitter;
import io.reactivex.rxjava3.core.ObservableOnSubscribe;
import io.reactivex.rxjava3.core.Observer;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class WeatherManager {
private static final String TAG = "WeatherManager";
@SuppressLint("StaticFieldLeak")
private static WeatherManager INSTANCE;
private Context mContext;
private QWeather mQWeather;
private Map<String, String> LocationIDMap = new HashMap<>();
private boolean loadCsvFinish = false;
private String mAdCode;
private WeatherManager(Context context) {
this.mContext = context.getApplicationContext();
initCsv();
try {
// 通过SDK提供的JWTGenerator设置令牌生成器其实现自TokenGenerator接口
JWTGenerator jwt = new JWTGenerator(NativeUtils.getQWeatherPrivateKey(), // 私钥
NativeUtils.getQWeatherProjectId(), // 项目ID
NativeUtils.getQWeatherCredentialId()); // 凭据ID
mQWeather = QWeather.getInstance(context, NativeUtils.getQWeatherUrl()) // 初始化服务地址
.setLogEnable(BuildConfig.DEBUG) // 启用调试日志(生产环境建议设置为 false
.setTokenGenerator(jwt);
} catch (Throwable e) {
Log.e(TAG, "QWeatherUtils: " + e.getMessage());
e.printStackTrace();
}
}
public static void init(Context context) {
if (INSTANCE == null) {
INSTANCE = new WeatherManager(context);
}
}
public static WeatherManager getInstance() {
if (INSTANCE == null) {
throw new IllegalStateException("You must be init WeatherManager first");
}
return INSTANCE;
}
public QWeather getQWeather() {
return mQWeather;
}
public void setAdCode(String adCode) {
mAdCode = adCode;
if (loadCsvFinish) {
if (!TextUtils.isEmpty(adCode)) {
String locationId = LocationIDMap.get(adCode);
Logger.e(this, TAG, "setAdCode: locationId = " + locationId);
LiveEventBus.get(CommonConfig.LOCATION_ID_CHANGED_KEY)
.post(locationId);
}
}
}
private void initCsv() {
Observable.create(new ObservableOnSubscribe<List<CityInfo>>() {
@Override
public void subscribe(@NonNull ObservableEmitter<List<CityInfo>> emitter) throws Throwable {
long time = System.currentTimeMillis();
List<CityInfo> cityInfos = CsvDeserializer.deserializeFromAssets(mContext, "China-City-List-latest.csv");
Log.e(TAG, "subscribe: deserializeFromAssets time = " + (System.currentTimeMillis() - time) + "ms");
emitter.onNext(cityInfos);
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<List<CityInfo>>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
Logger.e(TAG, "initCsv", "onSubscribe: ");
}
@Override
public void onNext(@NonNull List<CityInfo> cityInfos) {
Logger.e(TAG, "initCsv", "onNext: cityInfos size = " + cityInfos.size());
Logger.e(TAG, "initCsv", "onNext: cityInfos 0 info = " + cityInfos.get(0));
LocationIDMap = cityInfos.stream()
.filter(cityInfo -> cityInfo.getLocation_ID() != null) // 过滤掉 value 为 null 的条目
.collect(Collectors.toMap(CityInfo::getAD_code, CityInfo::getLocation_ID, (oldVal, newVal) -> oldVal, HashMap::new));
Logger.e(TAG, "initCsv", "onNext: LocationIDMap size = " + LocationIDMap.size());
onComplete();
}
@Override
public void onError(@NonNull Throwable e) {
Logger.e(TAG, "initCsv", "onError: " + e.getMessage());
}
@Override
public void onComplete() {
Logger.e(TAG, "initCsv", "onComplete: ");
loadCsvFinish = true;
if (!TextUtils.isEmpty(mAdCode)) {
String locationId = LocationIDMap.get(mAdCode);
Logger.e(TAG, "initCsv", "onComplete: locationId = " + locationId);
LiveEventBus.get(CommonConfig.LOCATION_ID_CHANGED_KEY)
.post(locationId);
}
}
});
}
private String getLocationID(String adCode) {
if (LocationIDMap != null) {
return LocationIDMap.get(adCode);
}
return "";
}
public void getWeatherNow(String adCode, Callback<WeatherNowResponse> callback) {
String locationID = getLocationID(adCode);
Log.e(TAG, "getWeatherNow: locationID = " + locationID);
WeatherParameter parameter = new WeatherParameter(locationID)
.lang(Lang.ZH_HANS)
.unit(Unit.METRIC);
mQWeather.weatherNow(parameter, callback);
}
public void getWeather10D(String adCode, Callback<WeatherDailyResponse> callback) {
String locationID = getLocationID(adCode);
Log.e(TAG, "getWeather10D: locationID = " + locationID);
WeatherParameter parameter = new WeatherParameter(locationID)
.lang(Lang.ZH_HANS)
.unit(Unit.METRIC);
mQWeather.weather10d(parameter, callback);
}
}

View File

@@ -1,12 +1,14 @@
package com.ttstd.dialer.push.jpush;
import android.content.Context;
import android.util.Log;
import cn.jpush.android.api.CustomMessage;
import cn.jpush.android.api.JPushMessage;
import cn.jpush.android.service.JPushMessageReceiver;
public class PushMessageService extends JPushMessageReceiver {
private static final String TAG = "PushMessageService";
@Override
public void onMessage(Context context, CustomMessage customMessage) {
@@ -16,5 +18,13 @@ public class PushMessageService extends JPushMessageReceiver {
@Override
public void onAliasOperatorResult(Context context, JPushMessage jPushMessage) {
super.onAliasOperatorResult(context, jPushMessage);
int errorCode = jPushMessage.getErrorCode();
Log.e(TAG, "onAliasOperatorResult: " + errorCode);
if (errorCode == 0) {
Log.e(TAG, "onAliasOperatorResult: " + jPushMessage.getAlias());
} else {
}
}
}

View File

@@ -1,29 +0,0 @@
package com.ttstd.dialer.utils;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DataUtil {
private static SimpleDateFormat day = new SimpleDateFormat("M月d日");
private static SimpleDateFormat hour = new SimpleDateFormat("HH:mm");
private static SimpleDateFormat minute = new SimpleDateFormat("mm");
/**
* 格式化日期(精确到天)
*/
public static String formatDateDay() {
return day.format(new Date());
}
public static String formatDateTime() {
return day.format(new Date());
}
/**
* 格式化日期(hour)
*/
public static String formatDateHour() {
return hour.format(new Date());
}
}

View File

@@ -0,0 +1,187 @@
package com.ttstd.dialer.utils;
import android.os.Build;
import androidx.annotation.RequiresApi;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.TextStyle;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
public class DateUtil {
private static SimpleDateFormat day = new SimpleDateFormat("M月d日");
private static SimpleDateFormat hour = new SimpleDateFormat("HH:mm");
private static SimpleDateFormat minute = new SimpleDateFormat("mm");
/**
* 格式化日期(精确到天)
*/
public static String formatDateDay() {
return day.format(new Date());
}
public static String formatDateTime() {
return day.format(new Date());
}
/**
* 格式化日期(hour)
*/
public static String formatDateHour() {
return hour.format(new Date());
}
public static boolean isTodayAll(String dateStr) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return isToday26(dateStr);
} else {
return isToday(dateStr);
}
}
public static boolean isToday(String dateStr) {
try {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
Date inputDate = sdf.parse(dateStr);
Calendar today = Calendar.getInstance();
Calendar target = Calendar.getInstance();
target.setTime(inputDate);
return today.get(Calendar.YEAR) == target.get(Calendar.YEAR) &&
today.get(Calendar.MONTH) == target.get(Calendar.MONTH) &&
today.get(Calendar.DAY_OF_MONTH) == target.get(Calendar.DAY_OF_MONTH);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@RequiresApi(api = Build.VERSION_CODES.O)
public static boolean isToday26(String dateStr) {
try {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDate inputDate = LocalDate.parse(dateStr, formatter);
LocalDate today = LocalDate.now();
return today.isEqual(inputDate);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public static String getWeekDayFromDateAll(String dateStr, Locale locale) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return getWeekDayFromDate26(dateStr,locale);
}else {
return getWeekDayFromDate(dateStr,locale);
}
}
/**
* 将 yyyy-MM-dd 格式的日期字符串转换为星期几
* @param dateStr 日期字符串格式yyyy-MM-dd
* @param locale 地区设置,如 Locale.CHINA
* @return 星期几的中文表示(如:星期一)
*/
@RequiresApi(api = Build.VERSION_CODES.O)
public static String getWeekDayFromDate26(String dateStr, Locale locale) {
// 定义日期格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
// 解析日期字符串
LocalDate date = LocalDate.parse(dateStr, formatter);
// 获取星期几并格式化为中文
return date.getDayOfWeek().getDisplayName(TextStyle.FULL, locale);
}
/**
* 将 yyyy-MM-dd 格式的日期字符串转换为星期几
* @param dateStr 日期字符串格式yyyy-MM-dd
* @param locale 地区设置,如 Locale.CHINA
* @return 星期几的中文表示
*/
public static String getWeekDayFromDate(String dateStr, Locale locale) {
try {
// 定义日期格式
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", locale);
Date date = sdf.parse(dateStr);
// 创建用于格式化星期的 SimpleDateFormat
SimpleDateFormat weekFormat = new SimpleDateFormat("EEEE", locale);
return weekFormat.format(date);
} catch (Exception e) {
e.printStackTrace();
return "";
}
}
public static String convertToChineseWeekdayAll(String dateStr) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return convertToChineseWeekdayWithLocalDate(dateStr);
}else {
return convertToChineseWeekday(dateStr);
}
}
/**
* 将yyyy-MM-dd格式的日期字符串转换为"周几"格式
* 例如2026-02-17 → 周二
*/
public static String convertToChineseWeekday(String dateStr) {
try {
// 解析日期
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.CHINA);
Date date = sdf.parse(dateStr);
if (date == null) {
return "日期解析失败";
}
// 使用Calendar获取星期几
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
// Calendar中周日=1,周一=2,...,周六=7
// 转换为中文"周几"格式
String[] weekDays = {"周日", "周一", "周二", "周三", "周四", "周五", "周六"};
// 注意Calendar的DAY_OF_WEEK从周日开始1=周日所以要减1
return weekDays[dayOfWeek - 1];
} catch (Exception e) {
e.printStackTrace();
return "日期格式错误";
}
}
/**
* 另一种实现方式使用LocalDate需要API 26+
* 推荐在Android 8.0及以上使用
*/
@RequiresApi(api = Build.VERSION_CODES.O)
public static String convertToChineseWeekdayWithLocalDate(String dateStr) {
try {
// 注意以下代码需要Android API 26+Android 8.0
java.time.LocalDate localDate = java.time.LocalDate.parse(dateStr);
int dayOfWeekValue = localDate.getDayOfWeek().getValue();
// java.time中周一=1,周二=2,...,周日=7
String[] weekDays = {"周一", "周二", "周三", "周四", "周五", "周六", "周日"};
return weekDays[dayOfWeekValue - 1];
} catch (Exception e) {
e.printStackTrace();
return "日期格式错误";
}
}
}

View File

@@ -0,0 +1,128 @@
package com.ttstd.dialer.utils;
import android.content.Context;
import android.text.TextUtils;
import android.util.Log;
import com.ttstd.dialer.BuildConfig;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
public class Logger {
// 日志级别枚举
public enum LogLevel {
DEBUG, INFO, WARN, ERROR
}
private static final String TAG = "AppLogger";
private static final String LOG_FILE_NAME = "app_log.txt";
private static File logFile;
private static LogLevel logLevel = LogLevel.DEBUG; // 默认日志级别
private static boolean isDebugMode = BuildConfig.DEBUG; // 默认关闭Debug输出
// 初始化日志系统(需在应用启动时调用)
public static void initialize(Context context, boolean debugMode) {
isDebugMode = debugMode;
try {
File storageDir = context.getExternalFilesDir("log");
logFile = new File(storageDir, LOG_FILE_NAME);
} catch (Exception e) {
Log.e(TAG, "Log file init failed: " + e.getMessage());
}
}
// 设置全局日志级别
public static void setLogLevel(LogLevel level) {
logLevel = level;
}
// 核心日志方法
private static void log(LogLevel level, Object context, String name, String message) {
if (level.ordinal() < logLevel.ordinal()) return; // 低于设定级别不记录
String tag;
if (TextUtils.isEmpty(context.getClass().getSimpleName())) {
tag = context.toString();
} else {
tag = context.getClass().getSimpleName();
}
String logMsg = formatLog(level, tag, name, message);
writeToFile(logMsg); // 始终写入文件
if (isDebugMode) {
outputToConsole(level, tag, name, message); // 仅Debug模式输出到控制台
}
}
// 格式化日志(含时间戳、线程信息)
private static String formatLog(LogLevel level, String tag, String name, String message) {
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()).format(new Date());
String threadName = Thread.currentThread().getName();
return String.format("%s [%s] %s/%s: %s %s\n", time, threadName, level.name(), tag, name, message);
}
// 写入日志文件
private static void writeToFile(String logMsg) {
if (logFile == null) return;
try (FileWriter writer = new FileWriter(logFile, true)) {
writer.append(logMsg);
} catch (IOException e) {
Log.e(TAG, "File write failed: " + e.getMessage());
}
}
// 输出到Android控制台Logcat
private static void outputToConsole(LogLevel level, String tag, String name, String message) {
switch (level) {
case DEBUG:
Log.d(tag, name + " : " + message);
break;
case INFO:
Log.i(tag, name + " : " + message);
break;
case WARN:
Log.w(tag, name + " : " + message);
break;
case ERROR:
Log.e(tag, name + " : " + message);
break;
}
}
// 快捷调用方法自动使用类名作为TAG
public static void d(Object context, String name, String message) {
log(LogLevel.DEBUG, context.getClass().getSimpleName(), name, message);
}
public static void i(Object context, String name, String message) {
log(LogLevel.INFO, context.getClass().getSimpleName(), name, message);
}
public static void w(Object context, String name, String message) {
log(LogLevel.WARN, context.getClass().getSimpleName(), name, message);
}
public static void e(Object context, String name, String message) {
log(LogLevel.ERROR, context, name, message);
}
public static void d(Object context, String name) {
log(LogLevel.DEBUG, context.getClass().getSimpleName(), name, "");
}
public static void i(Object context, String name) {
log(LogLevel.INFO, context.getClass().getSimpleName(), name, "");
}
public static void w(Object context, String name) {
log(LogLevel.WARN, context.getClass().getSimpleName(), name, "");
}
public static void e(Object context, String name) {
log(LogLevel.ERROR, context.getClass().getSimpleName(), name, "");
}
}

View File

@@ -0,0 +1,51 @@
package com.ttstd.dialer.utils;
public class NativeUtils {
static {
System.loadLibrary("ttstd");
}
/**
* sn加密key
*
* @return
*/
public static native String getKey();
/**
* 获取 获取绑定状态的code
*
* @return
*/
public static native String getBindStatuSigCode();
/**
* 和风天气API Host
*
* @return
*/
public static native String getQWeatherUrl();
/**
* 和风天气私钥
*
* @return
*/
public static native String getQWeatherPrivateKey();
/**
* 和风天气项目ID
*
* @return
*/
public static native String getQWeatherProjectId();
/**
* 和风天气凭据ID
*
* @return
*/
public static native String getQWeatherCredentialId();
}

View File

@@ -1,12 +1,61 @@
package com.ttstd.dialer.utils;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.content.Context;
import android.os.Build;
import android.util.Log;
import java.lang.reflect.Method;
import java.util.List;
public class SystemUtils {
/**
* 获取设备序列号
*
* @return
*/
@SuppressLint("MissingPermission")
public static String getSerial() {
String serial = "unknow";
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {//9.0+
serial = Build.getSerial();
} else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {//8.0+
serial = Build.SERIAL;
} else {//8.0-
Class<?> c = Class.forName("android.os.SystemProperties");
Method get = c.getMethod("get", String.class);
serial = (String) get.invoke(c, "ro.serialno");
}
} catch (Exception e) {
e.printStackTrace();
Log.e("e", "读取设备序列号异常:" + e.toString());
}
return serial;
}
/**
* 获取系统配置信息
*
* @param key
* @param defaultValue
* @return
*/
public static String getProperty(String key, String defaultValue) {
String value = defaultValue;
try {
Class<?> c = Class.forName("android.os.SystemProperties");
Method get = c.getMethod("get", String.class, String.class);
value = (String) (get.invoke(c, key, "unknown"));
} catch (Exception e) {
e.printStackTrace();
} finally {
return value;
}
}
public static boolean isMainProcessName(Context cxt, int pid) {
String packageName = cxt.getPackageName();
ActivityManager am = (ActivityManager) cxt.getSystemService(Context.ACTIVITY_SERVICE);
@@ -21,4 +70,6 @@ public class SystemUtils {
}
return false;
}
}

View File

@@ -0,0 +1,50 @@
#include <jni.h>
#include <string>
#include <sys/system_properties.h>
// 日志打印
#include <android/log.h>
#define LOG_TAG "TAG_LOG"
#define LOGI(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
extern "C"
JNIEXPORT jstring JNICALL
Java_com_ttstd_dialer_utils_NativeUtils_getKey(JNIEnv *env, jclass clazz) {
std::string key = "Ls0HSh9fNfeZ8Qu5";
return env->NewStringUTF(key.c_str());
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_ttstd_dialer_utils_NativeUtils_getBindStatuSigCode(JNIEnv *env, jclass clazz) {
std::string key = "tongtongstudio";
return env->NewStringUTF(key.c_str());
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_ttstd_dialer_utils_NativeUtils_getQWeatherUrl(JNIEnv *env, jclass clazz) {
std::string key = "nr4wcrp6av.re.qweatherapi.com";
return env->NewStringUTF(key.c_str());
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_ttstd_dialer_utils_NativeUtils_getQWeatherPrivateKey(JNIEnv *env, jclass clazz) {
std::string key = "MC4CAQAwBQYDK2VwBCIEIBSTXI5L8P9x6fhhFn7zC82dMkDisIqb/QBlUOU20YTl";
return env->NewStringUTF(key.c_str());
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_ttstd_dialer_utils_NativeUtils_getQWeatherProjectId(JNIEnv *env, jclass clazz) {
std::string key = "44KXE67QXE";
return env->NewStringUTF(key.c_str());
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_ttstd_dialer_utils_NativeUtils_getQWeatherCredentialId(JNIEnv *env, jclass clazz) {
std::string key = "KB5C8PK5TY";
return env->NewStringUTF(key.c_str());
}

View File

@@ -0,0 +1,114 @@
<?xml version="1.0" encoding="utf-8"?>
<layout 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"
tools:context=".activity.weather.main.WeatherMainActivity">
<data>
<variable
name="click"
type="com.ttstd.dialer.activity.weather.main.WeatherMainActivity.BtnClick" />
<variable
name="weatherNow"
type="com.qweather.sdk.response.weather.WeatherNow" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/coordinatorLayout"
android:layout_width="match_parent"
android:layout_height="180dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/tv_location"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:maxLines="1"
android:singleLine="true"
android:textColor="@color/white"
android:textSize="30sp"
tools:text="北京" />
<TextView
android:id="@+id/tv_temp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="4dp"
android:maxLines="1"
android:singleLine="true"
android:text="@{weatherNow.temp+`°`}"
android:textColor="@color/white"
android:textSize="40sp"
tools:text="N/A°" />
<TextView
android:id="@+id/tv_weather_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:maxLines="1"
android:singleLine="true"
android:text="@{weatherNow.text}"
android:textColor="@color/white"
android:textSize="20sp"
tools:text="晴" />
<TextView
android:id="@+id/tv_temp_range"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:maxLines="1"
android:singleLine="true"
android:textColor="@color/white"
android:textSize="20sp"
tools:text="最高10° 最低0°" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/coordinatorLayout">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -24,31 +24,149 @@
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:shadowColor="#80000000"
android:shadowDx="2"
android:shadowDy="2"
android:shadowRadius="2"
android:text="00:00"
android:textColor="@color/black"
android:textSize="55sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="00:00" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical">
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:shadowColor="#80000000"
android:shadowDx="2"
android:shadowDy="2"
android:shadowRadius="2"
android:text="00:00"
android:textColor="@color/black"
android:textSize="55sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="00:00" />
<TextView
android:id="@+id/tv_week"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:shadowColor="#80000000"
android:shadowDx="2"
android:shadowDy="2"
android:shadowRadius="2"
android:textColor="@color/black"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="星期一" />
<TextView
android:id="@+id/tv_lunar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:shadowColor="#80000000"
android:shadowDx="2"
android:shadowDy="2"
android:shadowRadius="2"
android:textColor="@color/black"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="正月初一" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:onClick="@{click::openWeather}"
android:orientation="vertical">
<TextView
android:id="@+id/tv_location"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:shadowColor="#80000000"
android:shadowDx="2"
android:shadowDy="2"
android:shadowRadius="2"
android:textColor="@color/black"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="北京市" />
<TextView
android:id="@+id/tv_temp_current"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:shadowColor="#80000000"
android:shadowDx="2"
android:shadowDy="2"
android:shadowRadius="2"
android:textColor="@color/black"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="N/A°" />
<TextView
android:id="@+id/tv_weather"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:shadowColor="#80000000"
android:shadowDx="2"
android:shadowDy="2"
android:shadowRadius="2"
android:textColor="@color/black"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="晴" />
<TextView
android:id="@+id/tv_temp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:shadowColor="#80000000"
android:shadowDx="2"
android:shadowDy="2"
android:shadowRadius="2"
android:textColor="@color/black"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="最低0° 最高10°" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"

View File

@@ -0,0 +1,88 @@
<?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="wrap_content">
<View
android:id="@+id/view"
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:background="@color/gray"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="48dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/view">
<TextView
android:id="@+id/tv_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:maxLines="1"
android:minEms="4"
android:textColor="@color/white"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="今天" />
<ImageView
android:id="@+id/iv_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="24dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/tv_date"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="24dp"
android:layout_marginEnd="48dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/iv_icon">
<TextView
android:id="@+id/tv_temp_min"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:maxLines="1"
android:textColor="@color/white"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="0°" />
<TextView
android:id="@+id/tv_temp_max"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:textColor="@color/white"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="10°" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -22,4 +22,13 @@
<item name="colorAccent">@color/colorAccent</item>
<item name="android:fitsSystemWindows">true</item>
</style>
<style name="AppThemeBlack" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/black</item>
<item name="colorPrimaryDark">@color/black</item>
<item name="colorAccent">@color/black</item>
<item name="android:fitsSystemWindows">true</item>
</style>
</resources>

View File

@@ -24,6 +24,13 @@ buildscript {
}
allprojects {
configurations.all {
resolutionStrategy {
force 'androidx.constraintlayout:constraintlayout:2.0.4'
force 'androidx.annotation:annotation:1.1.0'
}
}
repositories {
google()
// mavenCentral()