diff --git a/app/build.gradle b/app/build.gradle index d0325d3..886d165 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -22,7 +22,7 @@ android { applicationId "com.ttstd.dialer" //There are no CERT files because If the mini sdk version is 23+, the AGP will ignore the V1 scheme signature. -// minSdkVersion 23 + minSdkVersion 23 targetSdkVersion 37 versionCode 1 versionName "1.0" @@ -98,20 +98,19 @@ android { } } - flavorDimensions "version" - productFlavors { - // 用于正常开发和发布的版本 - normal { - dimension "version" - minSdkVersion 23 // 你的原始最低版本 - } - // 专门用于调试高版本特性的版本 - debugApi26 { - dimension "version" - minSdkVersion 31 // 为了使用 Database Inspector - } - } - +// flavorDimensions "version" +// productFlavors { +// // 用于正常开发和发布的版本 +// normal { +// dimension "version" +// minSdkVersion 23 // 你的原始最低版本 +// } +// // 专门用于调试高版本特性的版本 +// debugApi26 { +// dimension "version" +// minSdkVersion 31 // 为了使用 Database Inspector +// } +// } signingConfigs { keypub { @@ -232,6 +231,10 @@ dependencies { implementation "androidx.viewpager2:viewpager2:1.1.0" implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation "androidx.work:work-runtime:2.9.0" + implementation 'androidx.browser:browser:1.8.0' + implementation "androidx.webkit:webkit:1.14.0" + // Room依赖 implementation "androidx.room:room-runtime:2.8.4" implementation "androidx.room:room-rxjava3:2.8.4" @@ -358,8 +361,6 @@ dependencies { kapt 'com.arialyy.aria:compiler:3.8.15' //状态栏透明 implementation 'com.gitee.zackratos:UltimateBarX:0.8.0' - //指示器 - implementation 'com.github.hackware1993:MagicIndicator:1.7.0' implementation 'com.opencsv:opencsv:5.12.0' // 吐司框架:https://github.com/getActivity/Toaster @@ -368,6 +369,24 @@ dependencies { implementation 'com.github.getActivity:XXPermissions:20.0' implementation 'com.github.zcweng:switch-button:0.0.3@aar' implementation "com.github.kongzue.DialogX:DialogX:0.0.50" + implementation 'cn.6tail:lunar:1.7.7' + implementation "io.github.cymchad:BaseRecyclerViewAdapterHelper4:4.4.0" + implementation 'io.github.youth5201314:banner:2.2.3' + + // TinyPinyin核心包,约80KB + implementation 'com.github.promeg:tinypinyin:2.0.3' + // 可选,适用于Android的中国地区词典 + implementation 'com.github.promeg:tinypinyin-lexicons-android-cncity:2.0.3' + + // PictureSelector 基础 (必须) + implementation 'io.github.lucksiege:pictureselector:v3.11.2' + // 图片压缩 (按需引入) + implementation 'io.github.lucksiege:compress:v3.11.2' + // 图片裁剪 (按需引入) + implementation 'io.github.lucksiege:ucrop:v3.11.2' + // 自定义相机 (按需引入) + implementation 'io.github.lucksiege:camerax:v3.11.2' + } // 在 dependencies 之后添加 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b45c3d6..c9a409a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ @@ -23,6 +24,13 @@ + + + + + + + @@ -104,13 +112,18 @@ android:name=".activity.settings.utils.SettingsUtilsActivity" android:launchMode="singleTask" android:screenOrientation="portrait" /> + - - - - - - + + + + + + + + + + + + + + + - - - + + + + + = Build.VERSION_CODES.O_MR1) { + setShowWhenLocked(true); + setTurnScreenOn(true); + } else { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + setContentView(R.layout.activity_alarm_alert); + + Button btnStop = findViewById(R.id.btn_stop_alarm); + btnStop.setOnClickListener(v -> { + if (ringtone != null && ringtone.isPlaying()) { + ringtone.stop(); + } + finish(); + }); + + playAlarmRingtone(); + } + + private void playAlarmRingtone() { + Uri alarmUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM); + if (alarmUri == null) { + alarmUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE); + } + + ringtone = RingtoneManager.getRingtone(this, alarmUri); + if (ringtone != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + AudioAttributes aa = new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_ALARM) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .build(); + ringtone.setAudioAttributes(aa); + } + ringtone.play(); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (ringtone != null && ringtone.isPlaying()) { + ringtone.stop(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ttstd/dialer/activity/app/AppListActivity.java b/app/src/main/java/com/ttstd/dialer/activity/app/AppListActivity.java index cb6ba8b..968a66c 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/app/AppListActivity.java +++ b/app/src/main/java/com/ttstd/dialer/activity/app/AppListActivity.java @@ -11,8 +11,10 @@ import com.ttstd.dialer.R; import com.ttstd.dialer.adapter.MoreAppAdapter; import com.ttstd.dialer.base.mvvm.BaseMvvmActivity; import com.ttstd.dialer.config.CommonConfig; +import com.ttstd.dialer.config.LiveDataAction; import com.ttstd.dialer.databinding.ActivityAppListBinding; import com.ttstd.dialer.db.app.AppInfo; +import com.ttstd.dialer.livedata.LiveDataBus; import java.util.List; @@ -64,6 +66,11 @@ public class AppListActivity extends BaseMvvmActivityon(LiveDataAction.ACTION_UPDATE_APPS) + .observe(this, event -> { + mViewModel.getDbAppList(); + }); + mViewModel.mDesktopSortAppData.observe(this, new Observer>() { @Override public void onChanged(List appInfos) { diff --git a/app/src/main/java/com/ttstd/dialer/activity/contact/add/ContactAddActivity.java b/app/src/main/java/com/ttstd/dialer/activity/contact/add/ContactAddActivity.java index 722ff17..b8796c1 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/contact/add/ContactAddActivity.java +++ b/app/src/main/java/com/ttstd/dialer/activity/contact/add/ContactAddActivity.java @@ -1,21 +1,55 @@ package com.ttstd.dialer.activity.contact.add; +import android.content.Intent; import android.text.InputFilter; +import android.text.TextUtils; +import android.util.Log; import android.view.View; +import androidx.activity.result.ActivityResult; +import androidx.activity.result.ActivityResultCallback; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.lifecycle.Observer; -import com.hjq.toast.Toaster; +import com.kongzue.dialogx.dialogs.PopTip; +import com.kongzue.dialogx.dialogs.WaitDialog; +import com.luck.picture.lib.basic.PictureSelector; +import com.luck.picture.lib.config.SelectMimeType; +import com.luck.picture.lib.entity.LocalMedia; import com.ttstd.dialer.R; import com.ttstd.dialer.base.mvvm.BaseMvvmActivity; import com.ttstd.dialer.databinding.ActivityContactAddBinding; import com.ttstd.dialer.db.contact.ContactInfo; import com.ttstd.dialer.filter.NoSpaceInputFilter; import com.ttstd.dialer.mdm.DeviceManagerService; +import com.ttstd.dialer.utils.GlideUtils; import com.ttstd.dialer.utils.PhoneUtils; +import com.ttstd.dialer.view.GlideEngine; + +import java.util.ArrayList; public class ContactAddActivity extends BaseMvvmActivity { + private static final String TAG = "ContactAddActivity"; + + private ActivityResultLauncher launcherResult = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), + new ActivityResultCallback() { + @Override + public void onActivityResult(ActivityResult result) { + int resultCode = result.getResultCode(); + if (resultCode == RESULT_OK) { + ArrayList selectList = PictureSelector.obtainSelectorList(result.getData()); +// analyticalSelectResults(selectList); + Log.i(TAG, "onActivityResult PictureSelector size = " + selectList.size()); + GlideUtils.loadBitmapSafe(ContactAddActivity.this, selectList.get(0).getRealPath(), mViewDataBinding.nvAvatar); + } else if (resultCode == RESULT_CANCELED) { + Log.i(TAG, "onActivityResult PictureSelector Cancel"); + } + } + }); + ; + @Override public boolean setNightMode() { return true; @@ -44,6 +78,8 @@ public class ContactAddActivity extends BaseMvvmActivity 0) { - Toaster.show("添加成功"); + PopTip.show("添加成功").iconSuccess(); finish(); } else { @@ -62,6 +98,7 @@ public class ContactAddActivity extends BaseMvvmActivity() { @Override public void onChanged(Long aLong) { + PopTip.show("添加成功").iconSuccess(); finish(); } }); @@ -76,20 +113,26 @@ public class ContactAddActivity extends BaseMvvmActivity { Logger.e(TAG, "网络异常,降级到本地保存: " + throwable.getMessage()); - return saveContactObservable(contactInfo); + return saveContactDbObservable(contactInfo); }) .compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY)) .subscribe(new Observer() { @@ -80,7 +80,7 @@ public class ContactAddViewModel extends BaseViewModel saveContactObservable(ContactInfo contactInfo) { + private Observable saveContactDbObservable(ContactInfo contactInfo) { return Observable.create((ObservableOnSubscribe) emitter -> { Logger.d(TAG, "执行本地保存: " + contactInfo); contactInfo.setPosition(mRepository.getTotalCount() + 1); @@ -92,8 +92,8 @@ public class ContactAddViewModel extends BaseViewModel() { diff --git a/app/src/main/java/com/ttstd/dialer/activity/main/MainActivity.java b/app/src/main/java/com/ttstd/dialer/activity/main/MainActivity.java index 0c1bb38..c7b4981 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/main/MainActivity.java +++ b/app/src/main/java/com/ttstd/dialer/activity/main/MainActivity.java @@ -1,9 +1,6 @@ package com.ttstd.dialer.activity.main; -import android.content.BroadcastReceiver; -import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; import android.util.Log; import android.view.KeyEvent; import android.view.Window; @@ -20,12 +17,14 @@ import com.ttstd.dialer.R; import com.ttstd.dialer.adapter.AppGridAdapter; import com.ttstd.dialer.base.mvvm.BaseMvvmActivity; import com.ttstd.dialer.config.CommonConfig; +import com.ttstd.dialer.config.LiveDataAction; import com.ttstd.dialer.databinding.ActivityMainBinding; import com.ttstd.dialer.db.app.AppInfo; import com.ttstd.dialer.fragment.app.AppFragment; import com.ttstd.dialer.fragment.contact.ContactFragment; import com.ttstd.dialer.fragment.home.HomeFragment; import com.ttstd.dialer.fragment.settings.SettingsFragment; +import com.ttstd.dialer.livedata.LiveDataBus; import com.ttstd.dialer.manager.AppManager; import com.ttstd.dialer.manager.WeatherUpdateManager; import com.ttstd.dialer.service.main.MainService; @@ -157,7 +156,7 @@ public class MainActivity extends BaseMvvmActivity() { + WeatherManager.getInstance().getWeather10DayAdCode(adCode, new Callback() { @Override public void onSuccess(WeatherDailyResponse weatherDailyResponse) { Logger.e("getWeather10D", "onSuccess: "); diff --git a/app/src/main/java/com/ttstd/dialer/adapter/AppAdapter.java b/app/src/main/java/com/ttstd/dialer/adapter/AppAdapter.java index 83646ec..15ac5bd 100644 --- a/app/src/main/java/com/ttstd/dialer/adapter/AppAdapter.java +++ b/app/src/main/java/com/ttstd/dialer/adapter/AppAdapter.java @@ -16,12 +16,13 @@ import androidx.loader.app.LoaderManager; import androidx.loader.content.Loader; import androidx.recyclerview.widget.RecyclerView; +import com.kongzue.dialogx.dialogs.PopTip; import com.shehuan.niv.NiceImageView; import com.tencent.mmkv.MMKV; import com.ttstd.dialer.R; import com.ttstd.dialer.config.CommonConfig; import com.ttstd.dialer.db.app.AppInfo; -import com.ttstd.dialer.fragment.dialog.shortcut.ShortcutDialogFagment; +import com.ttstd.dialer.fragment.dialog.shortcut.MoveAppDialogFagment; import com.ttstd.dialer.utils.ApkUtils; import com.ttstd.dialer.utils.Logger; import com.ttstd.iconloader.IconCacheManager; @@ -82,29 +83,49 @@ public class AppAdapter extends RecyclerView.Adapter imple holder.root.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - ApkUtils.openApp(mContext, appInfo.getComponentName()); + if (!ApkUtils.openApp(mContext, appInfo.getComponentName())) { + PopTip.show("打开失败"); + } } }); holder.root.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { - ShortcutDialogFagment shortcutDialogFagment = new ShortcutDialogFagment(appInfo); - shortcutDialogFagment.setTitil("温馨提示"); - shortcutDialogFagment.setTips("是否将应用放入更多应用"); - shortcutDialogFagment.setOnClickListener(new ShortcutDialogFagment.OnClickListener() { +// ShortcutDialogFagment shortcutDialogFagment = new ShortcutDialogFagment(appInfo); +// shortcutDialogFagment.setTitile("温馨提示"); +// shortcutDialogFagment.setTips("是否把这个应用移到“更多应用”里?"); +// shortcutDialogFagment.setOnClickListener(new ShortcutDialogFagment.OnClickListener() { +// @Override +// public void onPositiveClick() { +// if (mShortcutCallback != null) +// mShortcutCallback.setAppInside(appInfo); +// shortcutDialogFagment.dismiss(); +// } +// +// @Override +// public void onNegativeClick() { +// shortcutDialogFagment.dismiss(); +// } +// }); +// shortcutDialogFagment.show(mContext.getSupportFragmentManager(), "ShortcutDialogFagment"); + + MoveAppDialogFagment moveAppDialogFagment = new MoveAppDialogFagment(appInfo); + moveAppDialogFagment.setTitile("温馨提示"); + moveAppDialogFagment.setTips("是否把这个应用移到“更多应用”里?"); + moveAppDialogFagment.setOnClickListener(new MoveAppDialogFagment.OnClickListener() { @Override public void onPositiveClick() { if (mShortcutCallback != null) mShortcutCallback.setAppInside(appInfo); - shortcutDialogFagment.dismiss(); + moveAppDialogFagment.dismiss(); } @Override public void onNegativeClick() { - shortcutDialogFagment.dismiss(); + moveAppDialogFagment.dismiss(); } }); - shortcutDialogFagment.show(mContext.getSupportFragmentManager(), "ShortcutDialogFagment"); + moveAppDialogFagment.show(mContext.getSupportFragmentManager(), "MoveAppDialogFagment"); return false; } }); diff --git a/app/src/main/java/com/ttstd/dialer/adapter/AppGridAdapter.java b/app/src/main/java/com/ttstd/dialer/adapter/AppGridAdapter.java index bb2ab4f..d3cb686 100644 --- a/app/src/main/java/com/ttstd/dialer/adapter/AppGridAdapter.java +++ b/app/src/main/java/com/ttstd/dialer/adapter/AppGridAdapter.java @@ -1,7 +1,6 @@ package com.ttstd.dialer.adapter; import android.content.ComponentName; -import android.content.Context; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.view.LayoutInflater; @@ -111,8 +110,8 @@ public class AppGridAdapter extends BaseAdapter implements LoaderManager.LoaderC @Override public boolean onLongClick(View v) { ShortcutDialogFagment shortcutDialogFagment = new ShortcutDialogFagment(appInfo); - shortcutDialogFagment.setTitil("温馨提示"); - shortcutDialogFagment.setTips("是否将应用放入更多应用"); + shortcutDialogFagment.setTitile("温馨提示"); + shortcutDialogFagment.setTips("是否把这个应用移到“更多应用”里?"); shortcutDialogFagment.setOnClickListener(new ShortcutDialogFagment.OnClickListener() { @Override public void onPositiveClick() { diff --git a/app/src/main/java/com/ttstd/dialer/adapter/HourlyWeatherAdapter.java b/app/src/main/java/com/ttstd/dialer/adapter/HourlyWeatherAdapter.java index 3499a82..89ce048 100644 --- a/app/src/main/java/com/ttstd/dialer/adapter/HourlyWeatherAdapter.java +++ b/app/src/main/java/com/ttstd/dialer/adapter/HourlyWeatherAdapter.java @@ -1,5 +1,7 @@ package com.ttstd.dialer.adapter; +import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade; + import android.graphics.drawable.PictureDrawable; import android.view.LayoutInflater; import android.view.View; @@ -16,13 +18,12 @@ import com.qweather.sdk.response.weather.WeatherHourly; import com.ttstd.dialer.R; import com.ttstd.dialer.glide.GlideApp; import com.ttstd.dialer.glide.svg.SvgSoftwareLayerSetter; +import com.ttstd.dialer.utils.GlideUtils; import com.ttstd.dialer.utils.Logger; import com.ttstd.dialer.utils.TimeUtils; import java.util.List; -import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade; - public class HourlyWeatherAdapter extends RecyclerView.Adapter { private static final String TAG = "HourlyWeatherAdapter"; @@ -57,7 +58,7 @@ public class HourlyWeatherAdapter extends RecyclerView.Adapter { private static final String TAG = "WeatherAdapter"; @@ -71,7 +72,7 @@ public class WeatherAdapter extends RecyclerView.Adapter= Build.VERSION_CODES.S) { + if (!alarmManager.canScheduleExactAlarms()) { + // 如果没有权限,跳转到系统设置页让用户授权 + Intent intent = new Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + return; + } + } + + Intent intent = new Intent(context, AlarmReceiver.class); + // 使用 FLAG_IMMUTABLE 或 FLAG_MUTABLE 增强 Android 12+ 安全性 + int flags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE : + PendingIntent.FLAG_UPDATE_CURRENT; + + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, ALARM_REQUEST_CODE, intent, flags); + + // 核心兼容性定时逻辑 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // Android 6.0+ 支持低功耗休眠模式(Doze Mode)下的精准唤醒 + alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + // Android 4.4 - 5.1 精准唤醒 + alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent); + } else { + // Android 4.4 以下 + alarmManager.set(AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent); + } + } + + /** + * 取消闹钟 + */ + public static void cancelAlarm(Context context) { + AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + if (alarmManager != null) { + Intent intent = new Intent(context, AlarmReceiver.class); + int flags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE : + PendingIntent.FLAG_UPDATE_CURRENT; + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, ALARM_REQUEST_CODE, intent, flags); + alarmManager.cancel(pendingIntent); + } + } +} diff --git a/app/src/main/java/com/ttstd/dialer/alarmclock/AlarmRepeatConfig.java b/app/src/main/java/com/ttstd/dialer/alarmclock/AlarmRepeatConfig.java new file mode 100644 index 0000000..191e3a5 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/alarmclock/AlarmRepeatConfig.java @@ -0,0 +1,34 @@ +package com.ttstd.dialer.alarmclock; + +import java.util.ArrayList; +import java.util.Calendar; + +public class AlarmRepeatConfig { + public static final int REPEAT_ONCE = 0; // 只响一次 + public static final int REPEAT_EVERYDAY = 1; // 每天 + public static final int REPEAT_WEEKDAYS = 2; // 周一至周五 + public static final int REPEAT_CUSTOM = 3; // 自定义星期几(比如只选周六日) + + private int repeatType; + private ArrayList customDays; // 存储 Calendar.MONDAY (2) 到 Calendar.SATURDAY (7) + + public AlarmRepeatConfig(int repeatType) { + this.repeatType = repeatType; + this.customDays = new ArrayList<>(); + if (repeatType == REPEAT_WEEKDAYS) { + customDays.add(Calendar.MONDAY); + customDays.add(Calendar.TUESDAY); + customDays.add(Calendar.WEDNESDAY); + customDays.add(Calendar.THURSDAY); + customDays.add(Calendar.FRIDAY); + } + } + + public int getRepeatType() { + return repeatType; + } + + public ArrayList getCustomDays() { + return customDays; + } +} diff --git a/app/src/main/java/com/ttstd/dialer/alarmclock/AlarmTimeCalculator.java b/app/src/main/java/com/ttstd/dialer/alarmclock/AlarmTimeCalculator.java new file mode 100644 index 0000000..4898a84 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/alarmclock/AlarmTimeCalculator.java @@ -0,0 +1,69 @@ +package com.ttstd.dialer.alarmclock; + +import java.util.ArrayList; +import java.util.Calendar; + +public class AlarmTimeCalculator { + + /** + * 根据重复配置,计算下一次闹钟应该响铃的绝对时间(毫秒) + * + * @param hour 闹钟设定的小时 + * @param minute 闹钟设定的分钟 + * @param config 重复配置 + * @return 下一次响铃的时间戳 + */ + public static long calculateNextAlarmTime(int hour, int minute, AlarmRepeatConfig config) { + Calendar now = Calendar.getInstance(); + + Calendar target = Calendar.getInstance(); + target.set(Calendar.HOUR_OF_DAY, hour); + target.set(Calendar.MINUTE, minute); + target.set(Calendar.SECOND, 0); + target.set(Calendar.MILLISECOND, 0); + + // 情况 1:只响一次 + if (config.getRepeatType() == AlarmRepeatConfig.REPEAT_ONCE) { + // 如果设定的时间在今天已经过去了,就定在明天 + if (target.getTimeInMillis() <= now.getTimeInMillis()) { + target.add(Calendar.DAY_OF_MONTH, 1); + } + return target.getTimeInMillis(); + } + + // 情况 2:每天 + if (config.getRepeatType() == AlarmRepeatConfig.REPEAT_EVERYDAY) { + if (target.getTimeInMillis() <= now.getTimeInMillis()) { + target.add(Calendar.DAY_OF_MONTH, 1); + } + return target.getTimeInMillis(); + } + + // 情况 3 & 4:周一至周五 或 自定义星期 + if (config.getRepeatType() == AlarmRepeatConfig.REPEAT_WEEKDAYS || + config.getRepeatType() == AlarmRepeatConfig.REPEAT_CUSTOM) { + + ArrayList targetDays = config.getCustomDays(); + + // 从今天开始,往后最多循环7天,找到最近的符合星期要求的日子 + for (int i = 0; i < 7; i++) { + int currentDayOfWeek = target.get(Calendar.DAY_OF_WEEK); + + // 如果 target 的日子在今天或今天之后,且其星期在用户选中的列表中 + if (targetDays.contains(currentDayOfWeek)) { + // 如果刚好是今天,但时间已经过去了,则不能算今天,继续往后找 + if (i == 0 && target.getTimeInMillis() <= now.getTimeInMillis()) { + target.add(Calendar.DAY_OF_MONTH, 1); + continue; + } + // 找到了最近的匹配日期 + return target.getTimeInMillis(); + } + // 否则,日期加一天,继续匹配 + target.add(Calendar.DAY_OF_MONTH, 1); + } + } + + return target.getTimeInMillis(); + } +} diff --git a/app/src/main/java/com/ttstd/dialer/base/BaseApplication.java b/app/src/main/java/com/ttstd/dialer/base/BaseApplication.java index a48c4b1..6f31231 100644 --- a/app/src/main/java/com/ttstd/dialer/base/BaseApplication.java +++ b/app/src/main/java/com/ttstd/dialer/base/BaseApplication.java @@ -3,6 +3,8 @@ package com.ttstd.dialer.base; import android.annotation.SuppressLint; import android.app.Application; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.os.Build; import android.os.Handler; import android.os.Looper; @@ -12,16 +14,19 @@ import androidx.multidex.MultiDex; import com.alibaba.android.arouter.launcher.ARouter; import com.arialyy.aria.core.Aria; import com.hjq.toast.Toaster; +import com.kongzue.dialogx.DialogX; import com.tencent.bugly.crashreport.CrashReport; import com.tencent.mmkv.MMKV; import com.ttstd.dialer.BuildConfig; import com.ttstd.dialer.config.CommonConfig; +import com.ttstd.dialer.config.SystemIntentAction; import com.ttstd.dialer.manager.AppManager; import com.ttstd.dialer.manager.MapManager; import com.ttstd.dialer.manager.WeatherManager; import com.ttstd.dialer.mdm.DeviceManagerService; import com.ttstd.dialer.network.OkHttpManager; import com.ttstd.dialer.push.PushExecutor; +import com.ttstd.dialer.receiver.AppChangedReceiver; import com.ttstd.dialer.utils.Logger; import com.ttstd.dialer.utils.NativeUtils; import com.ttstd.dialer.utils.SystemUtils; @@ -69,6 +74,22 @@ public class BaseApplication extends Application { init(); } + @Override + public void onTerminate() { + super.onTerminate(); + unregisterReceivers(); + } + + @Override + public void onLowMemory() { + super.onLowMemory(); + } + + @Override + public void onTrimMemory(int level) { + super.onTrimMemory(level); + } + private void init() { Logger.e(TAG, "init: "); Logger.e(TAG, "init: getNonce = " + NativeUtils.getNonce()); @@ -93,6 +114,7 @@ public class BaseApplication extends Application { // 初始化 Toast 框架 Toaster.init(this); + DialogX.init(this); Logger.e(TAG, "slowInit: "); Aria.init(this); @@ -107,6 +129,7 @@ public class BaseApplication extends Application { WeatherManager.init(this); IconCacheManager.init(this); + registerReceivers(); } } @@ -160,5 +183,38 @@ public class BaseApplication extends Application { }); } + private void registerReceivers() { + registerAppChangedReceive(); + } + + @Deprecated + private void unregisterReceivers() { + if (mAppChangedReceiver != null) { + unregisterReceiver(mAppChangedReceiver); + } + } + + private AppChangedReceiver mAppChangedReceiver; + + + private void registerAppChangedReceive() { + if (null == mAppChangedReceiver) { + mAppChangedReceiver = new AppChangedReceiver(); + } + IntentFilter filter = new IntentFilter(); + filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); + filter.addAction(Intent.ACTION_PACKAGE_INSTALL); + filter.addAction(Intent.ACTION_PACKAGE_ADDED); + filter.addAction(Intent.ACTION_PACKAGE_REPLACED); + filter.addAction(Intent.ACTION_MY_PACKAGE_REPLACED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED); + filter.addAction(Intent.ACTION_PACKAGE_CHANGED); + filter.addAction(SystemIntentAction.ACTION_PACKAGE_ENABLE_ROLLBACK); + filter.addAction(SystemIntentAction.ACTION_CANCEL_ENABLE_ROLLBACK); + filter.addAction(SystemIntentAction.ACTION_ROLLBACK_COMMITTED); + filter.addDataScheme("package"); + registerReceiver(mAppChangedReceiver, filter); + } } diff --git a/app/src/main/java/com/ttstd/dialer/config/LiveDataAction.java b/app/src/main/java/com/ttstd/dialer/config/LiveDataAction.java new file mode 100644 index 0000000..9bb52b8 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/config/LiveDataAction.java @@ -0,0 +1,5 @@ +package com.ttstd.dialer.config; + +public class LiveDataAction { + public static final String ACTION_UPDATE_APPS = "update_apps"; +} diff --git a/app/src/main/java/com/ttstd/dialer/config/SystemIntentAction.java b/app/src/main/java/com/ttstd/dialer/config/SystemIntentAction.java new file mode 100644 index 0000000..d6a70bc --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/config/SystemIntentAction.java @@ -0,0 +1,11 @@ +package com.ttstd.dialer.config; + +import android.annotation.SystemApi; + +public class SystemIntentAction { + public static final String ACTION_PACKAGE_ENABLE_ROLLBACK = "android.intent.action.PACKAGE_ENABLE_ROLLBACK"; + public static final String ACTION_CANCEL_ENABLE_ROLLBACK = "android.intent.action.CANCEL_ENABLE_ROLLBACK"; + @SystemApi + public static final String ACTION_ROLLBACK_COMMITTED = "android.intent.action.ROLLBACK_COMMITTED"; + +} diff --git a/app/src/main/java/com/ttstd/dialer/db/app/AppDao.java b/app/src/main/java/com/ttstd/dialer/db/app/AppDao.java index bbe06c3..fbbc8e2 100644 --- a/app/src/main/java/com/ttstd/dialer/db/app/AppDao.java +++ b/app/src/main/java/com/ttstd/dialer/db/app/AppDao.java @@ -12,21 +12,21 @@ import java.util.List; public interface AppDao { // 基本查询:获取总行数 @Query("SELECT COUNT(*) FROM app_list") - Integer getTotalCount(); + int getTotalCount(); // 查询:根据特定列的非空值计数 @Query("SELECT COUNT(id) FROM app_list") - Integer getCountById(); + int getCountById(); @Query("SELECT * FROM app_list WHERE package_name = :packageName AND class_name = :className") AppInfo getAppInfo(String packageName, String className); @Query("SELECT id FROM app_list WHERE package_name = :packageName AND class_name = :className") - Integer getIdByPackageAndClass(String packageName, String className); + int getIdByPackageAndClass(String packageName, String className); // 检查数据是否存在(返回布尔值) @Query("SELECT COUNT(*) FROM app_list WHERE package_name = :pkgName AND class_name = :clsName") - Integer checkAppInfoExists(String pkgName, String clsName); + int checkAppInfoExists(String pkgName, String clsName); @Insert long insert(AppInfo appInfo); @@ -35,16 +35,16 @@ public interface AppDao { long[] insert(List appInfos); @Update - Integer update(AppInfo appInfo); + int update(AppInfo appInfo); @Delete - Integer delete(AppInfo appInfo); + int delete(AppInfo appInfo); @Query("DELETE FROM app_list WHERE id = :id") - Integer deleteById(Integer id); + int deleteById(int id); @Query("DELETE FROM app_list") - Integer deleteAll(); + int deleteAll(); @Query("SELECT * FROM app_list ORDER BY position ASC") List getAllApp(); @@ -59,7 +59,7 @@ public interface AppDao { List getInsideApp(); @Query("SELECT * FROM app_list WHERE id = :id") - AppInfo getAppById(Integer id); + AppInfo getAppById(int id); @Query("SELECT * FROM app_list WHERE label LIKE :searchQuery OR package_name LIKE :searchQuery") List searchApp(String searchQuery); diff --git a/app/src/main/java/com/ttstd/dialer/fragment/dialog/shortcut/MoveAppDialogFagment.java b/app/src/main/java/com/ttstd/dialer/fragment/dialog/shortcut/MoveAppDialogFagment.java new file mode 100644 index 0000000..7e789a9 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/fragment/dialog/shortcut/MoveAppDialogFagment.java @@ -0,0 +1,186 @@ +package com.ttstd.dialer.fragment.dialog.shortcut; + +import android.app.Activity; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.Gravity; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; + +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; + +import com.ttstd.dialer.R; +import com.ttstd.dialer.base.mvvm.fragment.BaseMvvmDialogFragment; +import com.ttstd.dialer.databinding.FragmentDialogMoveAppBinding; +import com.ttstd.dialer.db.app.AppInfo; +import com.ttstd.dialer.utils.Logger; + +public class MoveAppDialogFagment extends BaseMvvmDialogFragment { + private static final String TAG = "ShortcutDialogFagment"; + + private Activity mContext; + private PackageManager mPackageManager; + + private AppInfo mAppInfo; + private String mTitile; + private String mTips; + private String mNegativeText; + private String mPositiveText; + + public MoveAppDialogFagment() { + } + + public MoveAppDialogFagment(AppInfo appInfo) { + mAppInfo = appInfo; + } + + public void setTitile(String titile) { + mTitile = titile; + } + + public void setTips(String tips) { + mTips = tips; + } + + public void setNegativeText(String negativeText) { + mNegativeText = negativeText; + } + + public void setPositiveText(String positiveText) { + mPositiveText = positiveText; + } + + public interface OnClickListener { + void onPositiveClick(); + + void onNegativeClick(); + } + + private OnClickListener mOnClickListener; + + public void setOnClickListener(OnClickListener onClickListener) { + mOnClickListener = onClickListener; + } + + @Override + protected int getLayoutId() { + return R.layout.fragment_dialog_move_app; + } + + @Override + protected void initDataBinding() { + mContext = getActivity(); + mPackageManager = mContext.getPackageManager(); + mViewModel.setContext(mContext); + mViewModel.setVDBinding(mViewDataBinding); + mViewModel.setLifecycle(getLifecycleSubject()); + mViewDataBinding.setClick(new BtnClick()); + } + + @Override + protected void initView(Bundle bundle) { + + } + + @Override + protected void initData(Bundle savedInstanceState) { + if (TextUtils.isEmpty(mTitile)) { + mViewDataBinding.tvTitle.setText("提示"); + } else { + mViewDataBinding.tvTitle.setText(mTitile); + } + + if (TextUtils.isEmpty(mTips)) { + mViewDataBinding.tvMessage.setText(""); + mViewDataBinding.tvMessage.setVisibility(View.GONE); + } else { + mViewDataBinding.tvMessage.setText(mTips); + mViewDataBinding.tvMessage.setVisibility(View.VISIBLE); + } + + if (TextUtils.isEmpty(mNegativeText)) { + mViewDataBinding.btnCancel.setText("取消"); + } else { + mViewDataBinding.btnCancel.setText(mNegativeText); + } + + if (TextUtils.isEmpty(mPositiveText)) { + mViewDataBinding.btnConfirm.setText("确定"); + } else { + mViewDataBinding.btnConfirm.setText(mPositiveText); + } + + if (mAppInfo != null) { + try { + ActivityInfo info = mPackageManager.getActivityInfo(mAppInfo.getComponentName(), 0); + mViewDataBinding.tvAppName.setText(info.loadLabel(mPackageManager)); + Drawable rawIcon = info.loadIcon(mPackageManager); + mViewDataBinding.ivAppIcon.setImageDrawable(rawIcon); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + } + } + + @Override + public void onStart() { + super.onStart(); + if (getDialog() != null) { + Window window = getDialog().getWindow(); + if (window == null) return; + WindowManager.LayoutParams params = window.getAttributes(); + params.width = WindowManager.LayoutParams.MATCH_PARENT; + params.height = WindowManager.LayoutParams.WRAP_CONTENT; + params.gravity = Gravity.CENTER; + window.setAttributes(params); + window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + getDialog().setCancelable(true); + getDialog().setCanceledOnTouchOutside(true); + } + } + + @Override + public void show(FragmentManager manager, String tag) { + DialogFragment fragment = (DialogFragment) manager.findFragmentByTag(tag); + if (fragment != null && fragment.isAdded() + && fragment.getDialog() != null && fragment.getDialog().isShowing()) { + return; + } + + try { + FragmentTransaction ft = manager.beginTransaction(); + ft.add(this, tag); + ft.commitAllowingStateLoss(); + } catch (Exception e) { + Logger.e(TAG, "show: " + e.getMessage()); + } + } + + @Override + public void fetchData() { + + } + + public class BtnClick { + public void onPositive(View view) { + if (mOnClickListener != null) { + mOnClickListener.onPositiveClick(); + } + } + + public void onNegative(View view) { + if (mOnClickListener != null) { + mOnClickListener.onNegativeClick(); + } + } + + } +} diff --git a/app/src/main/java/com/ttstd/dialer/fragment/dialog/shortcut/ShortcutDialogFagment.java b/app/src/main/java/com/ttstd/dialer/fragment/dialog/shortcut/ShortcutDialogFagment.java index 8d3364e..69908fe 100644 --- a/app/src/main/java/com/ttstd/dialer/fragment/dialog/shortcut/ShortcutDialogFagment.java +++ b/app/src/main/java/com/ttstd/dialer/fragment/dialog/shortcut/ShortcutDialogFagment.java @@ -30,7 +30,7 @@ public class ShortcutDialogFagment extends BaseMvvmDialogFragment> bus = new HashMap<>(); + + @SuppressWarnings("unchecked") + private MutableLiveData liveData(String key) { + synchronized (bus) { + MutableLiveData ld = bus.get(key); + if (ld == null) { + // singleLiveEvent 风格:只消费一次 + ld = new SingleLiveData<>(); + bus.put(key, ld); + } + return (MutableLiveData) ld; + } + } + + public void send(String key, T value) { + liveData(key).postValue(value); + } + + public LiveData on(String key) { + return liveData(key); + } + + // ===== 只消费一次的 SingleLiveData ===== + private static class SingleLiveData extends MutableLiveData { + private final AtomicBoolean pending = new AtomicBoolean(false); + + @Override + public void observe(@NonNull LifecycleOwner owner, + @NonNull Observer observer) { + super.observe(owner, t -> { + if (pending.compareAndSet(true, false)) { + observer.onChanged(t); + } + }); + } + + @Override + public void setValue(T value) { + pending.set(true); + super.setValue(value); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ttstd/dialer/manager/AppManager.java b/app/src/main/java/com/ttstd/dialer/manager/AppManager.java index 6639487..2a14b6f 100644 --- a/app/src/main/java/com/ttstd/dialer/manager/AppManager.java +++ b/app/src/main/java/com/ttstd/dialer/manager/AppManager.java @@ -15,9 +15,11 @@ import android.util.Log; import com.tencent.mmkv.MMKV; import com.ttstd.dialer.bean.ApkInstalledInfo; import com.ttstd.dialer.config.CommonConfig; +import com.ttstd.dialer.config.LiveDataAction; import com.ttstd.dialer.db.app.AppInfo; import com.ttstd.dialer.db.app.AppRepository; import com.ttstd.dialer.gson.GsonUtils; +import com.ttstd.dialer.livedata.LiveDataBus; import com.ttstd.dialer.utils.ApkUtils; import com.ttstd.dialer.utils.HashUtils; import com.ttstd.dialer.utils.Logger; @@ -398,23 +400,73 @@ public class AppManager { } public void updateApp(String packageName) { - List resolveInfos = ApkUtils.getResolveInfoByPackageName(mContext, packageName); - if (resolveInfos.isEmpty()) return; + boolean isInstalled = ApkUtils.isInstalled(mContext, packageName); - resolveInfos.stream() - .map(this::resolveInfoToDesktopApp) - .filter(Objects::nonNull) - .filter(app -> !isAppExists(app)) - .forEach(app -> { - app.setPosition(mAppRepository.getTotalCount()); - try { - mAppRepository.insert(app); - } catch (Exception e) { - Logger.e(TAG, "更新应用插入失败: " + app.getPackageName(), e); + if (isInstalled) { + Logger.i(TAG, "应用已安装: " + packageName + ", 开始更新数据库"); + + List resolveInfos = ApkUtils.getResolveInfoByPackageName(mContext, packageName); + + if (!resolveInfos.isEmpty()) { + resolveInfos.stream() + .map(this::resolveInfoToDesktopApp) + .filter(Objects::nonNull) + .forEach(app -> { + try { + int existingId = mAppRepository.getIdByPackageAndClass(app.getPackageName(), app.getClassName()); + + if (existingId > 0) { +// app.setId(existingId); +// mAppRepository.update(app); +// Logger.i(TAG, "更新应用信息: " + app.getPackageName()); + } else { + app.setOutside(1); + app.setPosition(mAppRepository.getTotalCount()); + mAppRepository.insert(app); + Logger.i(TAG, "新增应用信息: " + app.getPackageName()); + } + } catch (Exception e) { + Logger.e(TAG, "处理应用失败: " + app.getPackageName(), e); + } + }); + } else { + Logger.w(TAG, "应用已安装但无可启动的 Launcher Activity"); + } + } else { + Logger.i(TAG, "应用未安装或被禁用: " + packageName + ", 开始删除数据库数据"); + + try { + List allApps = mAppRepository.getAllApp(); + if (allApps != null) { + List idsToDelete = allApps.stream() + .filter(app -> packageName.equals(app.getPackageName())) + .map(AppInfo::getId) + .filter(id -> id > 0) + .collect(Collectors.toList()); + + if (!idsToDelete.isEmpty()) { + idsToDelete.forEach(id -> { + try { + mAppRepository.deleteById(id); + Logger.i(TAG, "删除应用数据库记录, ID: " + id); + } catch (Exception e) { + Logger.e(TAG, "删除应用记录失败, ID: " + id, e); + } + }); + Logger.i(TAG, "成功删除 " + idsToDelete.size() + " 条记录"); + } else { + Logger.i(TAG, "数据库中未找到该应用的记录"); } - }); + } + } catch (Exception e) { + Logger.e(TAG, "删除应用记录时发生异常", e); + } + } + + LiveDataBus.get().send(LiveDataAction.ACTION_UPDATE_APPS, TAG); } + // 重构getAllApp方法,复用现有处理逻辑 public void refreshAllApps() { CompletableFuture.runAsync(() -> { diff --git a/app/src/main/java/com/ttstd/dialer/manager/WeatherManager.java b/app/src/main/java/com/ttstd/dialer/manager/WeatherManager.java index dae59f5..3031231 100644 --- a/app/src/main/java/com/ttstd/dialer/manager/WeatherManager.java +++ b/app/src/main/java/com/ttstd/dialer/manager/WeatherManager.java @@ -77,7 +77,6 @@ public class WeatherManager { WeatherNowResponse weatherNowResponse = getWeatherNowCache(); if (weatherNowResponse != null) { mWeatherUpdateManager.publishWeatherNowUpdate(weatherNowResponse); - } WeatherHourlyResponse weatherHourlyResponse = getWeather24hCache(); if (weatherHourlyResponse != null) { diff --git a/app/src/main/java/com/ttstd/dialer/manager/WeatherUpdateManager.kt b/app/src/main/java/com/ttstd/dialer/manager/WeatherUpdateManager.kt index 65c1568..14393a6 100644 --- a/app/src/main/java/com/ttstd/dialer/manager/WeatherUpdateManager.kt +++ b/app/src/main/java/com/ttstd/dialer/manager/WeatherUpdateManager.kt @@ -101,4 +101,4 @@ class WeatherUpdateManager { .onEach { observer(it) } .launchIn(scope) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/ttstd/dialer/network/OperationResult.java b/app/src/main/java/com/ttstd/dialer/network/OperationResult.java new file mode 100644 index 0000000..9c7cb58 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/network/OperationResult.java @@ -0,0 +1,259 @@ +package com.ttstd.dialer.network; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class OperationResult { + /** + * 操作状态枚举 + */ + public enum Status { + SUCCESS, // 成功 + NETWORK_ERROR, // 网络错误 + SERVER_ERROR, // 服务器错误 + LOCAL_ERROR, // 本地错误 + SYNC_PARTIAL_SUCCESS // 部分成功(如:网络失败但本地成功) + } + + private Status status; + private String errorCode; + private String errorMessage; + + // 云端ID(在线同步返回) + private Long cloudId; + + // 本地ID(本地数据库返回) + private Long localId; + + // 数据来源标识 + private DataSource dataSource; + + // 是否需要重试 + private boolean needRetry; + + // 原始数据对象(可选) + private Object rawData; + + public enum DataSource { + CLOUD, // 来自云端 + LOCAL, // 来自本地 + BOTH, // 两者都有 + UNKNOWN // 未知 + } + + private OperationResult() { + } + + // ==================== 静态工厂方法 ==================== + + /** + * 创建成功的结果(云端) + */ + public static OperationResult successFromCloud(Long cloudId) { + OperationResult result = new OperationResult(); + result.status = Status.SUCCESS; + result.cloudId = cloudId; + result.dataSource = DataSource.CLOUD; + result.needRetry = false; + return result; + } + + /** + * 创建成功的结果(本地) + */ + public static OperationResult successFromLocal(Long localId) { + OperationResult result = new OperationResult(); + result.status = Status.SUCCESS; + result.localId = localId; + result.dataSource = DataSource.LOCAL; + result.needRetry = false; + return result; + } + + /** + * 创建成功的结果(云端+本地) + */ + public static OperationResult successBoth(Long cloudId, Long localId) { + OperationResult result = new OperationResult(); + result.status = Status.SUCCESS; + result.cloudId = cloudId; + result.localId = localId; + result.dataSource = DataSource.BOTH; + result.needRetry = false; + return result; + } + + /** + * 创建部分成功的结果(网络失败但本地成功) + */ + public static OperationResult partialSuccess(Long localId, String errorMsg) { + OperationResult result = new OperationResult(); + result.status = Status.SYNC_PARTIAL_SUCCESS; + result.localId = localId; + result.errorMessage = errorMsg; + result.dataSource = DataSource.LOCAL; + result.needRetry = true; + return result; + } + + /** + * 创建网络错误的结果 + */ + public static OperationResult networkError(String errorMsg) { + OperationResult result = new OperationResult(); + result.status = Status.NETWORK_ERROR; + result.errorCode = "NETWORK_ERROR"; + result.errorMessage = errorMsg; + result.dataSource = DataSource.UNKNOWN; + result.needRetry = true; + return result; + } + + /** + * 创建服务器错误的结果 + */ + public static OperationResult serverError(String errorCode, String errorMsg) { + OperationResult result = new OperationResult(); + result.status = Status.SERVER_ERROR; + result.errorCode = errorCode; + result.errorMessage = errorMsg; + result.dataSource = DataSource.UNKNOWN; + result.needRetry = true; + return result; + } + + /** + * 创建本地错误的结果 + */ + public static OperationResult localError(String errorMsg) { + OperationResult result = new OperationResult(); + result.status = Status.LOCAL_ERROR; + result.errorCode = "LOCAL_ERROR"; + result.errorMessage = errorMsg; + result.dataSource = DataSource.UNKNOWN; + result.needRetry = false; + return result; + } + + // ==================== Getter/Setter ==================== + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public String getErrorCode() { + return errorCode; + } + + public void setErrorCode(String errorCode) { + this.errorCode = errorCode; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public Long getCloudId() { + return cloudId; + } + + public void setCloudId(Long cloudId) { + this.cloudId = cloudId; + } + + public Long getLocalId() { + return localId; + } + + public void setLocalId(Long localId) { + this.localId = localId; + } + + public DataSource getDataSource() { + return dataSource; + } + + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + } + + public boolean isNeedRetry() { + return needRetry; + } + + public void setNeedRetry(boolean needRetry) { + this.needRetry = needRetry; + } + + @Nullable + public Object getRawData() { + return rawData; + } + + public void setRawData(@Nullable Object rawData) { + this.rawData = rawData; + } + + // ==================== 便捷判断方法 ==================== + + /** + * 是否完全成功 + */ + public boolean isSuccess() { + return status == Status.SUCCESS; + } + + /** + * 是否有错误 + */ + public boolean hasError() { + return status != Status.SUCCESS; + } + + /** + * 是否来自云端 + */ + public boolean isFromCloud() { + return dataSource == DataSource.CLOUD || dataSource == DataSource.BOTH; + } + + /** + * 是否来自本地 + */ + public boolean isFromLocal() { + return dataSource == DataSource.LOCAL || dataSource == DataSource.BOTH; + } + + /** + * 获取有效的ID(优先云端,其次本地) + */ + public Long getEffectiveId() { + if (cloudId != null) { + return cloudId; + } + return localId; + } + + @NonNull + @Override + public String toString() { + return "OperationResult{" + + "status=" + status + + ", errorCode='" + errorCode + '\'' + + ", errorMessage='" + errorMessage + '\'' + + ", cloudId=" + cloudId + + ", localId=" + localId + + ", dataSource=" + dataSource + + ", needRetry=" + needRetry + + '}'; + } + +} diff --git a/app/src/main/java/com/ttstd/dialer/receiver/AlarmReceiver.java b/app/src/main/java/com/ttstd/dialer/receiver/AlarmReceiver.java new file mode 100644 index 0000000..35216d9 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/receiver/AlarmReceiver.java @@ -0,0 +1,90 @@ +package com.ttstd.dialer.receiver; + +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Build; + +import androidx.core.app.NotificationCompat; + +import com.ttstd.dialer.activity.alarm.AlarmAlertActivity; +import com.ttstd.dialer.alarmclock.AlarmManagerHelper; +import com.ttstd.dialer.alarmclock.AlarmRepeatConfig; +import com.ttstd.dialer.alarmclock.AlarmTimeCalculator; + +public class AlarmReceiver extends BroadcastReceiver { + private static final String CHANNEL_ID = "alarm_channel"; + + @Override + public void onReceive(Context context, Intent intent) { + // 1. 执行原有的响铃和弹出通知/界面的逻辑 + // (调用上一次回答中创建的通知栏或全屏拉起逻辑) + showAlarmNotification(context); + + // 2. 核心:动态轮转,实现重复闹钟 + scheduleNextRepeatAlarm(context); + } + + private void scheduleNextRepeatAlarm(Context context) { + // 从 SharedPreferences 中取出用户之前保存的闹钟时间和重复设置 + // 实际开发中此处可以替换为 SQLite 数据库 + SharedPreferences sp = context.getSharedPreferences("AlarmPrefs", Context.MODE_PRIVATE); + int hour = sp.getInt("alarm_hour", 8); + int minute = sp.getInt("alarm_minute", 0); + int repeatType = sp.getInt("repeat_type", AlarmRepeatConfig.REPEAT_ONCE); + + // 如果是“只响一次”,响完就结束了,不需要再设置 + if (repeatType == AlarmRepeatConfig.REPEAT_ONCE) { + return; + } + + // 如果是 每天 或 周一至周五 + AlarmRepeatConfig config = new AlarmRepeatConfig(repeatType); + + // 重新计算下一次的时间(由于此时已经过了当前闹钟点,计算出的必定是未来的时间) + long nextTriggerTime = AlarmTimeCalculator.calculateNextAlarmTime(hour, minute, config); + + // 再次调用第一步写好的精准闹钟设置工具,埋下新的定时炸弹 + AlarmManagerHelper.setExactAlarm(context, nextTriggerTime); + } + + private void showAlarmNotification(Context context) { + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + if (notificationManager == null) return; + + // 1. 创建通知渠道 (Android 8.0+) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID, "闹钟响铃", NotificationManager.IMPORTANCE_HIGH); + channel.setDescription("用于展示闹钟响铃界面"); + channel.enableLights(true); + channel.setBypassDnd(true); // 绕过免打扰 + notificationManager.createNotificationChannel(channel); + } + + // 2. 构建点击通知或全屏弹出的 Intent + Intent alertIntent = new Intent(context, AlarmAlertActivity.class); + alertIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + + int flags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE : + PendingIntent.FLAG_UPDATE_CURRENT; + PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, alertIntent, flags); + + // 3. 构建高优先级通知(兼容 Android 10+ 后台全屏拉起) + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(android.R.drawable.ic_lock_idle_alarm) + .setContentTitle("闹钟响了") + .setContentText("时间到了,快起床!") + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setCategory(NotificationCompat.CATEGORY_ALARM) + .setAutoCancel(true) + .setFullScreenIntent(pendingIntent, true); // 核心:锁屏时直接拉起 Activity + + notificationManager.notify(1, builder.build()); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ttstd/dialer/receiver/AppChangedReceiver.java b/app/src/main/java/com/ttstd/dialer/receiver/AppChangedReceiver.java index 6b2c6ca..ff9899d 100644 --- a/app/src/main/java/com/ttstd/dialer/receiver/AppChangedReceiver.java +++ b/app/src/main/java/com/ttstd/dialer/receiver/AppChangedReceiver.java @@ -4,16 +4,24 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.text.TextUtils; +import android.util.Log; import com.tencent.mmkv.MMKV; import com.ttstd.dialer.config.CommonConfig; +import com.ttstd.dialer.manager.AppManager; import com.ttstd.dialer.utils.Logger; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + public class AppChangedReceiver extends BroadcastReceiver { - private static final String TAG = "ApkInstallReceiver"; + private static final String TAG = "AppChangedReceiver"; private MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE); + // 创建一个单线程池用于处理应用变更事件 + private static final ExecutorService sExecutor = Executors.newSingleThreadExecutor(); + @Override public void onReceive(final Context context, Intent intent) { String action = intent.getAction(); @@ -35,6 +43,13 @@ public class AppChangedReceiver extends BroadcastReceiver { default: break; } + sExecutor.execute(() -> { + try { + AppManager.getInstance().updateApp(packageName); + } catch (Exception e) { + Log.e(TAG, "onReceive: updateApp " + e.getMessage()); + } + }); } } diff --git a/app/src/main/java/com/ttstd/dialer/utils/ApkUtils.java b/app/src/main/java/com/ttstd/dialer/utils/ApkUtils.java index 32c6728..c7fb997 100644 --- a/app/src/main/java/com/ttstd/dialer/utils/ApkUtils.java +++ b/app/src/main/java/com/ttstd/dialer/utils/ApkUtils.java @@ -1,5 +1,6 @@ package com.ttstd.dialer.utils; +import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -94,7 +95,7 @@ public class ApkUtils { try { context.startActivity(intent); return true; - } catch (Exception e) { + } catch (ActivityNotFoundException e) { Logger.e(TAG, "openApp: " + e.getMessage()); } return false; diff --git a/app/src/main/java/com/ttstd/dialer/utils/GlideUtils.java b/app/src/main/java/com/ttstd/dialer/utils/GlideUtils.java index 1e0a17b..aca10b3 100644 --- a/app/src/main/java/com/ttstd/dialer/utils/GlideUtils.java +++ b/app/src/main/java/com/ttstd/dialer/utils/GlideUtils.java @@ -154,6 +154,21 @@ public class GlideUtils { .into(imageView); } + public static void loadImageSafe(@Nullable Context context, int id, + @NonNull ImageView imageView, + @DrawableRes int errorId) { + RequestBuilder requestManager = getSafeRequestManager(context); + if (requestManager == null) { + Logger.w(TAG, "Skip loading image due to invalid context"); + return; + } + + requestManager + .load(id) + .error(errorId) + .into(imageView); + } + public static void loadImageSafe(@Nullable Context context, @Nullable String url, @NonNull ImageView imageView, Drawable drawable) { diff --git a/app/src/main/java/com/ttstd/dialer/view/ClearEditText.java b/app/src/main/java/com/ttstd/dialer/view/ClearEditText.java index 0f2ad1b..fb07350 100644 --- a/app/src/main/java/com/ttstd/dialer/view/ClearEditText.java +++ b/app/src/main/java/com/ttstd/dialer/view/ClearEditText.java @@ -62,7 +62,7 @@ public class ClearEditText extends AppCompatEditText { } private void updateClearButton() { - Drawable endDrawable = length() > 0 ? clearDrawable : null; + Drawable endDrawable = (length() > 0 && hasFocus()) ? clearDrawable : null; setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, endDrawable, null); } diff --git a/app/src/main/java/com/ttstd/dialer/view/GlideEngine.java b/app/src/main/java/com/ttstd/dialer/view/GlideEngine.java new file mode 100644 index 0000000..82f8f8b --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/view/GlideEngine.java @@ -0,0 +1,117 @@ +package com.ttstd.dialer.view; + +import android.content.Context; +import android.widget.ImageView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.resource.bitmap.CenterCrop; +import com.bumptech.glide.load.resource.bitmap.RoundedCorners; +import com.luck.picture.lib.engine.ImageEngine; +import com.luck.picture.lib.utils.ActivityCompatHelper; +import com.ttstd.dialer.R; + +/** + * @author:luck + * @date:2019-11-13 17:02 + * @describe:Glide加载引擎 + */ +public class GlideEngine implements ImageEngine { + + /** + * 加载图片 + * + * @param context 上下文 + * @param url 资源url + * @param imageView 图片承载控件 + */ + @Override + public void loadImage(Context context, String url, ImageView imageView) { + if (!ActivityCompatHelper.assertValidRequest(context)) { + return; + } + Glide.with(context) + .load(url) + .into(imageView); + } + + @Override + public void loadImage(Context context, ImageView imageView, String url, int maxWidth, int maxHeight) { + if (!ActivityCompatHelper.assertValidRequest(context)) { + return; + } + Glide.with(context) + .load(url) + .override(maxWidth, maxHeight) + .into(imageView); + } + + /** + * 加载相册目录封面 + * + * @param context 上下文 + * @param url 图片路径 + * @param imageView 承载图片ImageView + */ + @Override + public void loadAlbumCover(Context context, String url, ImageView imageView) { + if (!ActivityCompatHelper.assertValidRequest(context)) { + return; + } + Glide.with(context) + .asBitmap() + .load(url) + .override(180, 180) + .sizeMultiplier(0.5f) + .transform(new CenterCrop(), new RoundedCorners(8)) + .placeholder(R.drawable.ps_image_placeholder) + .into(imageView); + } + + + /** + * 加载图片列表图片 + * + * @param context 上下文 + * @param url 图片路径 + * @param imageView 承载图片ImageView + */ + @Override + public void loadGridImage(Context context, String url, ImageView imageView) { + if (!ActivityCompatHelper.assertValidRequest(context)) { + return; + } + Glide.with(context) + .load(url) + .override(200, 200) + .centerCrop() + .placeholder(R.drawable.ps_image_placeholder) + .into(imageView); + } + + @Override + public void pauseRequests(Context context) { + if (!ActivityCompatHelper.assertValidRequest(context)) { + return; + } + Glide.with(context).pauseRequests(); + } + + @Override + public void resumeRequests(Context context) { + if (!ActivityCompatHelper.assertValidRequest(context)) { + return; + } + Glide.with(context).resumeRequests(); + } + + private GlideEngine() { + } + + private static final class InstanceHolder { + static final GlideEngine instance = new GlideEngine(); + } + + public static GlideEngine createGlideEngine() { + return InstanceHolder.instance; + } +} diff --git a/app/src/main/java/com/ttstd/dialer/view/SettingItem.java b/app/src/main/java/com/ttstd/dialer/view/SettingItem.java index 8169597..3e8d186 100644 --- a/app/src/main/java/com/ttstd/dialer/view/SettingItem.java +++ b/app/src/main/java/com/ttstd/dialer/view/SettingItem.java @@ -14,7 +14,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.constraintlayout.widget.ConstraintLayout; - import com.ttstd.dialer.R; import org.jetbrains.annotations.NotNull; @@ -74,6 +73,10 @@ public class SettingItem extends ConstraintLayout { requestLayout(); } + public boolean isChecked() { + return tb.isToggleOn(); + } + public SettingItem(@NonNull @NotNull Context context) { super(context); init(context, null); @@ -215,6 +218,12 @@ public class SettingItem extends ConstraintLayout { } + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + tb.setEnabled(enabled); + } + @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); diff --git a/app/src/main/res/drawable-xxhdpi/ps_ic_placeholder.png b/app/src/main/res/drawable-xxhdpi/ps_ic_placeholder.png new file mode 100644 index 0000000..1f7ad00 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ps_ic_placeholder.png differ diff --git a/app/src/main/res/drawable/bg_app_info_card.xml b/app/src/main/res/drawable/bg_app_info_card.xml new file mode 100644 index 0000000..ff32a4c --- /dev/null +++ b/app/src/main/res/drawable/bg_app_info_card.xml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_btn_cancel.xml b/app/src/main/res/drawable/bg_btn_cancel.xml new file mode 100644 index 0000000..7271177 --- /dev/null +++ b/app/src/main/res/drawable/bg_btn_cancel.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_btn_confirm.xml b/app/src/main/res/drawable/bg_btn_confirm.xml new file mode 100644 index 0000000..6763261 --- /dev/null +++ b/app/src/main/res/drawable/bg_btn_confirm.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_dialog_card.xml b/app/src/main/res/drawable/bg_dialog_card.xml new file mode 100644 index 0000000..a44de57 --- /dev/null +++ b/app/src/main/res/drawable/bg_dialog_card.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_tip_icon_circle.xml b/app/src/main/res/drawable/bg_tip_icon_circle.xml new file mode 100644 index 0000000..c8eb3a4 --- /dev/null +++ b/app/src/main/res/drawable/bg_tip_icon_circle.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml index 0eed934..d95d01b 100644 --- a/app/src/main/res/drawable/ic_add.xml +++ b/app/src/main/res/drawable/ic_add.xml @@ -5,8 +5,8 @@ android:viewportHeight="1024"> + android:fillColor="#ffffff" /> + android:fillColor="#ffffff" /> diff --git a/app/src/main/res/drawable/ps_image_placeholder.xml b/app/src/main/res/drawable/ps_image_placeholder.xml new file mode 100644 index 0000000..8c63cb1 --- /dev/null +++ b/app/src/main/res/drawable/ps_image_placeholder.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_alarm_alert.xml b/app/src/main/res/layout/activity_alarm_alert.xml new file mode 100644 index 0000000..4ed21de --- /dev/null +++ b/app/src/main/res/layout/activity_alarm_alert.xml @@ -0,0 +1,22 @@ + + + + + +