USER
package com.example.myapplication.api;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.os.AsyncTask;
import android.os.Build;
import android.util.Log;
import com.example.myapplication.CurrencyRate;
import com.example.myapplication.CurrencyRepository;
import org.json.JSONObject;
import org.json.JSONException;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.*;
public class LoadCurrencyRatesTask extends AsyncTask<Void, Void, Boolean> {
private static final String TAG = "LoadCurrencyRatesTask";
private static final String API_KEY = "05ZPKFI7IKOPLLRM"; // Замените на ваш API ключ Alpha Vantage
private final Context context;
private final CurrencyRepository repository;
private final Callback callback;
private final List<CurrencyRate> newRates = new ArrayList<>();
public interface Callback {
void onRatesLoaded();
void onError(Exception e);
}
public LoadCurrencyRatesTask(Context context, CurrencyRepository repository, Callback callback) {
this.context = context;
this.repository = repository;
this.callback = callback;
}
@Override
protected Boolean doInBackground(Void... voids) {
try {
if (isInternetAvailable()) {
Log.d(TAG, "Интернет-соединение доступно. Обновление курсов через API.");
List<String> currencies = Arrays.asList("EUR");
boolean allSuccess = true;
for (String targetCurrency : currencies) {
boolean success = fetchHistoricalRates("USD", targetCurrency);
if (!success) {
allSuccess = false;
Log.e(TAG, "Ошибка при загрузке курса для " + targetCurrency);
break; // Если требуется остановиться при первой ошибке
}
}
if (allSuccess) {
// Все запросы успешны, обновляем базу данных
repository.clearAllRates();
repository.insertCurrencyRatesSync(newRates);
// Вставка обратных курсов, если необходимо
for (CurrencyRate rate : newRates) {
if (!rate.baseCurrency.equals(rate.targetCurrency)) {
double reverseRate = rate.rate != 0 ? 1 / rate.rate : 0;
CurrencyRate reverseCurrencyRate = new CurrencyRate(rate.targetCurrency, rate.baseCurrency, reverseRate, rate.date);
repository.insertCurrencyRateSync(reverseCurrencyRate);
}
}
return true;
} else {
// Если хотя бы один запрос не удался, не обновляем базу данных
return false;
}
} else {
Log.d(TAG, "Нет интернет-соединения. Используются кэшированные данные из базы данных.");
return true; // Используем кэшированные данные
}
// return false;
} catch (Exception e) {
Log.e(TAG, "Ошибка при загрузке курсов валют", e);
return false;
}
}
@Override
protected void onPostExecute(Boolean success) {
if (success) {
if (callback != null) {
callback.onRatesLoaded();
}
} else {
if (callback != null) {
callback.onError(new Exception("Не удалось обновить курсы валют. Используются кэшированные данные."));
}
}
}
private boolean isInternetAvailable() {
ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (connectivityManager != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Network network = connectivityManager.getActiveNetwork();
if (network == null) {
Log.d(TAG, "Нет активной сети.");
return false;
}
NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network);
boolean hasInternet = capabilities != null &&
(capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET));
Log.d(TAG, "Интернет доступен: " + hasInternet);
return hasInternet;
} else {
// Для старых версий Android
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
boolean isConnected = networkInfo != null && networkInfo.isConnected();
Log.d(TAG, "Интернет доступен (старый метод): " + isConnected);
return isConnected;
}
}
Log.d(TAG, "ConnectivityManager равен null.");
return false;
}
/**
* Возвращает true, если курс валют успешно загружен и добавлен в список newRates.
*/
private boolean fetchHistoricalRates(String baseCurrency, String targetCurrency) {
try {
String apiUrl = String.format(
Locale.US,
"https://www.alphavantage.co/query?function=FX_DAILY&from_symbol=%s&to_symbol=%s&apikey=%s&outputsize=compact",
baseCurrency,
targetCurrency,
API_KEY
);
URL url = new URL(apiUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
int responseCode = connection.getResponseCode();
InputStream inputStream;
if (responseCode == HttpURLConnection.HTTP_OK) {
inputStream = connection.getInputStream();
} else {
inputStream = connection.getErrorStream();
Log.e(TAG, "HTTP ошибка при запросе к API: " + responseCode);
return false;
}
BufferedReader reader = new BufferedReader(
new InputStreamReader(inputStream)
);
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
reader.close();
String jsonResponseString = response.toString();
Log.d(TAG, "API Response для " + baseCurrency + "/" + targetCurrency + ": " + jsonResponseString);
JSONObject jsonResponse = new JSONObject(jsonResponseString);
if (jsonResponse.has("Error Message")) {
Log.e(TAG, "Ошибка при получении данных для " + targetCurrency + ": " + jsonResponse.getString("Error Message"));
return false;
}
if (jsonResponse.has("Note")) {
Log.e(TAG, "Достигнут лимит частоты API вызовов: " + jsonResponse.getString("Note"));
return false;
}
if (!jsonResponse.has("Time Series FX (Daily)")) {
Log.e(TAG, "Неожиданный формат JSON для валюты: " + targetCurrency);
return false;
}
JSONObject timeSeries = jsonResponse.getJSONObject("Time Series FX (Daily)");
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
Iterator<String> dates = timeSeries.keys();
while (dates.hasNext()) {
String dateStr = dates.next();
JSONObject dailyData = timeSeries.getJSONObject(dateStr);
double rate = dailyData.getDouble("4. close");
Date date = dateFormat.parse(dateStr);
CurrencyRate currencyRate = new CurrencyRate(baseCurrency, targetCurrency, rate, date);
newRates.add(currencyRate);
}
Log.d(TAG, "Исторические курсы получены для " + baseCurrency + "/" + targetCurrency);
return true;
} catch (JSONException e) {
Log.e(TAG, "Ошибка парсинга JSON для " + targetCurrency, e);
return false;
} catch (Exception e) {
Log.e(TAG, "Ошибка при получении исторических курсов для " + targetCurrency, e);
return false;
}
}
}
package com.example.myapplication;
import android.content.Context;
import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;
import androidx.room.TypeConverters;
@Database(entities = {CurrencyRate.class}, version = 4, exportSchema = false)
@TypeConverters({Converters.class})
public abstract class AppDatabase extends RoomDatabase {
private static volatile AppDatabase INSTANCE;
public abstract CurrencyRateDao currencyRateDao();
public static AppDatabase getDatabase(final Context context) {
if (INSTANCE == null) {
synchronized (AppDatabase.class) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
AppDatabase.class, "currency_database")
.fallbackToDestructiveMigration()
.build();
}
}
}
return INSTANCE;
}
}package com.example.myapplication;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.util.Log;
public class BootReceiver extends BroadcastReceiver {
private static final String TAG = "BootReceiver";
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
Log.d(TAG, "BOOT_COMPLETED получен");
SharedPreferences prefs = context.getSharedPreferences("preferences", Context.MODE_PRIVATE);
int hour = prefs.getInt("notification_hour", -1);
int minute = prefs.getInt("notification_minute", -1);
if (hour != -1 && minute != -1) {
CurrencyAlarmHelper.scheduleDailyNotification(context, hour, minute);
Log.d(TAG, "Повторное установление уведомления после перезагрузки");
}
}
}
}package com.example.myapplication;
import android.content.Intent; // Добавлено для обработки нажатия на кнопку
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.LinearLayout; // Добавлено для работы с time_range_layout
import android.widget.Spinner;
import android.widget.TextView; // Добавлено для работы с time_range_label
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.github.mikephil.charting.charts.LineChart;
import com.github.mikephil.charting.components.XAxis;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.data.LineData;
import com.github.mikephil.charting.data.LineDataSet;
import com.github.mikephil.charting.formatter.ValueFormatter;
import com.github.mikephil.charting.utils.ColorTemplate;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.button.MaterialButtonToggleGroup;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
public class ChartActivity extends AppCompatActivity {
private static final String TAG = "ChartActivity";
private CurrencyRepository repository;
private LineChart lineChart;
// Выбранные валюты
private Spinner spinnerFromChart;
private Spinner spinnerToChart;
private MaterialButton buttonSwapChart;
private MaterialButtonToggleGroup timeRangeToggleGroup;
private MaterialButton buttonBackToMain;
private MaterialButton buttonCurrencyList; // Новая кнопка
private boolean isRatesLoaded = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chart);
repository = new CurrencyRepository(this);
lineChart = findViewById(R.id.line_chart);
timeRangeToggleGroup = findViewById(R.id.time_range_toggle_group_chart);
buttonBackToMain = findViewById(R.id.button_back_to_main);
spinnerFromChart = findViewById(R.id.spinner_from_chart);
spinnerToChart = findViewById(R.id.spinner_to_chart);
buttonSwapChart = findViewById(R.id.button_swap_chart);
// Настройка обработчика кнопок выбора периода
timeRangeToggleGroup.addOnButtonCheckedListener((group, checkedId, isChecked) -> {
if (isChecked) {
Log.d(TAG, "Выбран период: " + checkedId);
loadChartData(); // Перезагрузка графика при изменении периода
}
});
// Настройка обработчика кнопки "Назад"
buttonBackToMain.setOnClickListener(v -> finish());
// Настройка обработчика кнопки свапа
buttonSwapChart.setOnClickListener(v -> {
swapCurrencies();
loadChartData();
});
// Настройка слушателя для выбора валют из спиннеров
AdapterView.OnItemSelectedListener spinnerListener = new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (isRatesLoaded) {
loadChartData();
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {}
};
spinnerFromChart.setOnItemSelectedListener(spinnerListener);
spinnerToChart.setOnItemSelectedListener(spinnerListener);
// Загрузка валют в спиннеры
new LoadCurrenciesTask().execute();
}
private void swapCurrencies() {
int fromPosition = spinnerFromChart.getSelectedItemPosition();
int toPosition = spinnerToChart.getSelectedItemPosition();
spinnerFromChart.setSelection(toPosition);
spinnerToChart.setSelection(fromPosition);
}
/**
* AsyncTask для загрузки списка валют после обновления курсов.
*/
private class LoadCurrenciesTask extends AsyncTask<Void, Void, List<String>> {
@Override
protected void onPreExecute() {
// Можно добавить индикатор загрузки, если необходимо
}
@Override
protected List<String> doInBackground(Void... voids) {
// Загрузка списка валют из репозитория
List<String> currencies = repository.getAllUniqueCurrencies();
Log.d("LoadCurrenciesTask", "Получены валюты: " + currencies.toString());
return currencies;
}
@Override
protected void onPostExecute(List<String> currencies) {
super.onPostExecute(currencies);
if (currencies != null && !currencies.isEmpty()) {
isRatesLoaded = true;
ArrayAdapter<String> adapterFrom = new ArrayAdapter<>(ChartActivity.this, android.R.layout.simple_spinner_item, currencies);
adapterFrom.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerFromChart.setAdapter(adapterFrom);
ArrayAdapter<String> adapterTo = new ArrayAdapter<>(ChartActivity.this, android.R.layout.simple_spinner_item, currencies);
adapterTo.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerToChart.setAdapter(adapterTo);
// Устанавливаем стандартные значения
String defaultFromCurrency = getIntent().getStringExtra("FROM_CURRENCY");
String defaultToCurrency = getIntent().getStringExtra("TO_CURRENCY");
if (defaultFromCurrency != null) {
int fromIndex = currencies.indexOf(defaultFromCurrency);
if (fromIndex >= 0) spinnerFromChart.setSelection(fromIndex);
}
if (defaultToCurrency != null) {
int toIndex = currencies.indexOf(defaultToCurrency);
if (toIndex >= 0) spinnerToChart.setSelection(toIndex);
}
// Устанавливаем кнопку по умолчанию в ToggleGroup
timeRangeToggleGroup.check(R.id.button_7_days_chart);
loadChartData();
} else {
Toast.makeText(ChartActivity.this, "Не удалось загрузить валюты.", Toast.LENGTH_SHORT).show();
Log.e("LoadCurrenciesTask", "Список валют пуст");
}
}
}
/**
* Загружает данные для графика на основе выбранных валют и периода.
*/
private void loadChartData() {
int checkedButtonId = timeRangeToggleGroup.getCheckedButtonId();
int timeRangeDays;
if (checkedButtonId == R.id.button_30_days_chart) {
timeRangeDays = 30;
} else if (checkedButtonId == R.id.button_90_days_chart) {
timeRangeDays = 90;
} else {
timeRangeDays = 7;
}
String fromCurrency = spinnerFromChart.getSelectedItem().toString().toUpperCase();
String toCurrency = spinnerToChart.getSelectedItem().toString().toUpperCase();
new LoadChartDataTask(fromCurrency, toCurrency, timeRangeDays).execute();
}
private class LoadChartDataTask extends AsyncTask<Void, Void, LineData> {
private final String fromCurrency;
private final String toCurrency;
private final int timeRangeDays;
private List<Date> dates;
public LoadChartDataTask(String fromCurrency, String toCurrency, int timeRangeDays) {
this.fromCurrency = fromCurrency;
this.toCurrency = toCurrency;
this.timeRangeDays = timeRangeDays;
}
@Override
protected LineData doInBackground(Void... voids) {
Date endDate = new Date(); // Текущая дата
Calendar cal = Calendar.getInstance();
cal.setTime(endDate);
cal.add(Calendar.DAY_OF_YEAR, -timeRangeDays);
Date startDate = cal.getTime();
List<CurrencyRate> historyFrom = repository.getCurrencyRateHistoryWithinDates("USD", fromCurrency, startDate, endDate);
List<CurrencyRate> historyTo = repository.getCurrencyRateHistoryWithinDates("USD", toCurrency, startDate, endDate);
if (fromCurrency.equals("USD")) {
historyFrom = generateUsdHistory(startDate, endDate);
}
if (toCurrency.equals("USD")) {
historyTo = generateUsdHistory(startDate, endDate);
}
if (historyFrom == null || historyTo == null || historyFrom.isEmpty() || historyTo.isEmpty()) {
return null;
}
int size = Math.min(historyFrom.size(), historyTo.size());
List<Entry> entries = new ArrayList<>();
dates = new ArrayList<>();
for (int i = 0; i < size; i++) {
CurrencyRate rateFrom = historyFrom.get(i);
CurrencyRate rateTo = historyTo.get(i);
double rateFromUSD = rateFrom.rate;
double rateToUSD = rateTo.rate;
double crossRate = rateToUSD / rateFromUSD;
entries.add(new Entry(i, (float) crossRate));
dates.add(rateFrom.date);
}
LineDataSet dataSet = new LineDataSet(entries, "Курс " + fromCurrency + "/" + toCurrency);
dataSet.setColor(ColorTemplate.getHoloBlue());
dataSet.setLineWidth(2f);
dataSet.setDrawCircles(false);
dataSet.setMode(LineDataSet.Mode.CUBIC_BEZIER);
dataSet.setDrawFilled(true);
dataSet.setFillAlpha(50);
dataSet.setFillColor(ColorTemplate.getHoloBlue());
dataSet.setDrawValues(false);
return new LineData(dataSet);
}
/**
* Генерирует список курсов USD/USD (стабильный курс 1.0).
*/
private List<CurrencyRate> generateUsdHistory(Date startDate, Date endDate) {
List<Date> allDates = repository.getAllDatesWithinRange(startDate, endDate);
List<CurrencyRate> usdHistory = new ArrayList<>();
for (Date date : allDates) {
usdHistory.add(new CurrencyRate("USD", "USD", 1.0, date));
}
return usdHistory;
}
@Override
protected void onPostExecute(LineData lineData) {
if (lineData != null && lineData.getEntryCount() > 0) {
XAxis xAxis = lineChart.getXAxis();
xAxis.setPosition(XAxis.XAxisPosition.BOTTOM);
xAxis.setLabelRotationAngle(-45f);
xAxis.setGranularity(1f);
xAxis.setDrawGridLines(false);
xAxis.setValueFormatter(new ValueFormatter() {
private final SimpleDateFormat mFormat = new SimpleDateFormat("dd MMM", Locale.getDefault());
@Override
public String getFormattedValue(float value) {
int index = (int) value;
if (index >= 0 && index < dates.size()) {
Date date = dates.get(index);
return mFormat.format(date);
} else {
return "";
}
}
});
// Настраиваем ось Y
lineChart.getAxisRight().setEnabled(false);
// Обновляем данные графика
lineChart.setData(lineData);
// Разрешаем взаимодействие
lineChart.setTouchEnabled(true);
lineChart.setDragEnabled(true);
lineChart.setScaleEnabled(true);
// Устанавливаем кастомный MarkerView (при наличии)
// Например:
// CustomMarkerView markerView = new CustomMarkerView(ChartActivity.this, R.layout.custom_marker_view, dates);
// markerView.setChartView(lineChart);
// lineChart.setMarker(markerView);
lineChart.invalidate();
} else {
lineChart.clear();
lineChart.invalidate();
Toast.makeText(ChartActivity.this, "Нет данных для отображения графика.", Toast.LENGTH_SHORT).show();
}
}
}
}package com.example.myapplication;
import android.os.AsyncTask;
import android.util.Log;
public class ConvertCurrencyTask extends AsyncTask<Void, Void, Double> {
private final String fromCurrency;
private final String toCurrency;
private final double amount;
private final CurrencyRepository repository;
private final Callback callback;
public interface Callback {
void onConversionResult(Double result);
}
public ConvertCurrencyTask(String fromCurrency, String toCurrency, double amount, CurrencyRepository repository, Callback callback) {
this.fromCurrency = fromCurrency;
this.toCurrency = toCurrency;
this.amount = amount;
this.repository = repository;
this.callback = callback;
}
@Override
protected Double doInBackground(Void... voids) {
// Get exchange rates relative to USD
Double rateFromUSD;
if (fromCurrency.equals("USD")) {
rateFromUSD = 1.0;
} else {
rateFromUSD = repository.getExchangeRate("USD", fromCurrency);
if (rateFromUSD != null) {
rateFromUSD = 1 / rateFromUSD; // Convert to rate fromCurrency -> USD
}
}
Double rateToUSD;
if (toCurrency.equals("USD")) {
rateToUSD = 1.0;
} else {
rateToUSD = repository.getExchangeRate("USD", toCurrency);
if (rateToUSD != null) {
rateToUSD = 1 / rateToUSD; // Convert to rate toCurrency -> USD
}
}
if (rateFromUSD == null || rateToUSD == null) {
Log.e("ConvertCurrencyTask", "Conversion rate not found for " + fromCurrency + " or " + toCurrency);
return null;
}
// Correct cross-rate calculation
double crossRate = rateFromUSD / rateToUSD;
// Perform conversion
return amount * crossRate;
}
@Override
protected void onPostExecute(Double result) {
callback.onConversionResult(result);
}
}package com.example.myapplication;
import androidx.room.TypeConverter;
import java.util.Date;
public class Converters {
@TypeConverter
public static Date fromTimestamp(Long value) {
return value == null ? null : new Date(value);
}
@TypeConverter
public static Long dateToTimestamp(Date date) {
return date == null ? null : date.getTime();
}
}package com.example.myapplication;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
/**
* Adapter class for managing and binding currency pairs to the RecyclerView.
*/
public class CurrencyAdapter extends RecyclerView.Adapter<CurrencyAdapter.ViewHolder> {
/**
* Interface to handle favorite (remove) button clicks.
*/
public interface OnFavoriteClickListener {
/**
* Called when the favorite button is clicked.
*
* @param currencyPair The currency pair that was clicked.
* @param isFavorite Indicates whether the pair is being added or removed from favorites.
*/
void onFavoriteClick(String currencyPair, boolean isFavorite);
}
private List<String> currencyPairs; // List of currency pairs to display
private OnFavoriteClickListener listener; // Listener for favorite button clicks
/**
* Constructor for the adapter.
*
* @param currencyPairs List of currency pairs to display.
* @param listener Listener to handle favorite button clicks.
*/
public CurrencyAdapter(List<String> currencyPairs, OnFavoriteClickListener listener) {
this.currencyPairs = currencyPairs;
this.listener = listener;
}
/**
* ViewHolder class that holds references to the views in each RecyclerView item.
*/
public static class ViewHolder extends RecyclerView.ViewHolder {
public TextView textViewPair; // TextView to display the currency pair
public ImageButton buttonFavorite; // Button to remove the pair from favorites
public ViewHolder(View view) {
super(view);
textViewPair = view.findViewById(R.id.text_view_pair);
buttonFavorite = view.findViewById(R.id.button_favorite);
}
}
@Override
public CurrencyAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// Inflate the item layout for each currency pair
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_currency_pair, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(CurrencyAdapter.ViewHolder holder, int position) {
String pair = currencyPairs.get(position); // Get the currency pair at this position
holder.textViewPair.setText(pair); // Set the currency pair text
// Set the favorite button icon as filled heart since all displayed pairs are favorites
holder.buttonFavorite.setImageResource(R.drawable.star);
// Handle favorite button click to remove the pair from favorites
holder.buttonFavorite.setOnClickListener(v -> {
// Notify the listener (CurrencyListActivity) to handle removal
listener.onFavoriteClick(pair, false);
});
}
@Override
public int getItemCount() {
return currencyPairs.size(); // Return the total number of currency pairs
}
/**
* Adds a new currency pair to the adapter and notifies the RecyclerView.
*
* @param pair The currency pair to add.
*/
public void addCurrencyPair(String pair) {
currencyPairs.add(pair); // Add the new pair to the list
notifyItemInserted(currencyPairs.size() - 1); // Notify the adapter of the new item
}
/**
* Removes a currency pair from the adapter at the specified position and notifies the RecyclerView.
*
* @param position The position of the currency pair to remove.
*/
public void removeCurrencyPair(int position) {
if (position >= 0 && position < currencyPairs.size()) {
currencyPairs.remove(position); // Remove the pair from the list
notifyItemRemoved(position); // Notify the adapter of the removed item
}
}
}package com.example.myapplication;
import android.app.AlarmManager;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.util.Log;
import java.util.Calendar;
/**
* Помощник для настройки и отмены уведомлений о курсах валют.
*/
public class CurrencyAlarmHelper {
private static final String TAG = "CurrencyAlarmHelper";
private static final String CHANNEL_ID = "daily_rate_channel";
private static final String CHANNEL_NAME = "Daily Rate Channel";
private static final String CHANNEL_DESCRIPTION = "Channel for daily exchange rate notifications";
/**
* Устанавливает ежедневное уведомление в указанное время.
*
* @param context Контекст приложения.
* @param hour Часы (24-часовой формат) для уведомления.
* @param minute Минуты для уведомления.
*/
public static void scheduleDailyNotification(Context context, int hour, int minute) {
// Создание Notification Channel (если необходимо)
createNotificationChannel(context);
// Отмена существующих уведомлений перед установкой новых
cancelAllNotifications(context);
// Получение AlarmManager
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
if (alarmManager == null) {
Log.e(TAG, "AlarmManager не доступен");
return;
}
// Создание Intent для DailyNotificationReceiver
Intent intent = new Intent(context, DailyNotificationReceiver.class);
PendingIntent pendingIntent = PendingIntent.getBroadcast(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
// Настройка времени срабатывания уведомления
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
calendar.set(Calendar.HOUR_OF_DAY, hour);
calendar.set(Calendar.MINUTE, minute);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
// Если время уже прошло, установить на следующий день
if (calendar.getTimeInMillis() <= System.currentTimeMillis()) {
calendar.add(Calendar.DAY_OF_MONTH, 1);
}
// Установка точного будильника
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Для API 23 и выше используем setExactAndAllowWhileIdle
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
calendar.getTimeInMillis(),
pendingIntent
);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// Для API от 19 до 22 используем setExact
alarmManager.setExact(
AlarmManager.RTC_WAKEUP,
calendar.getTimeInMillis(),
pendingIntent
);
} else {
// Для API ниже 19 используем set
alarmManager.set(
AlarmManager.RTC_WAKEUP,
calendar.getTimeInMillis(),
pendingIntent
);
}
Log.d(TAG, "Уведомление установлено на " + calendar.getTime().toString());
}
/**
* Отменяет все уведомления и будильники.
*
* @param context Контекст приложения.
*/
public static void cancelAllNotifications(Context context) {
// Получение AlarmManager
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
if (alarmManager == null) {
Log.e(TAG, "AlarmManager не доступен");
return;
}
// Создание Intent для DailyNotificationReceiver
Intent intent = new Intent(context, DailyNotificationReceiver.class);
PendingIntent pendingIntent = PendingIntent.getBroadcast(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
// Отмена будильника
alarmManager.cancel(pendingIntent);
Log.d(TAG, "Все уведомления и будильники отменены");
}
/**
* Создаёт Notification Channel для отправки уведомлений.
*
* @param context Контекст приложения.
*/
private static void createNotificationChannel(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Проверка, существует ли уже канал
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
if (notificationManager == null) {
Log.e(TAG, "NotificationManager не доступен");
return;
}
if (notificationManager.getNotificationChannel(CHANNEL_ID) == null) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
);
channel.setDescription(CHANNEL_DESCRIPTION);
notificationManager.createNotificationChannel(channel);
Log.d(TAG, "Notification Channel создан");
} else {
Log.d(TAG, "Notification Channel уже существует");
}
}
}
}package com.example.myapplication;
import android.Manifest;
import android.app.TimePickerDialog;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class CurrencyListActivity extends AppCompatActivity {
// RecyclerView для отображения списка избранных пар валют
private RecyclerView recyclerView;
// Адаптер для управления данными и связывания их с RecyclerView
private CurrencyAdapter currencyAdapter;
// Репозиторий для получения данных о валютах
private CurrencyRepository repository;
// Набор для хранения избранных пар валют
private Set<String> favoriteCurrencyPairs;
// TextView для отображения, когда нет избранных пар
private TextView textEmpty;
// TextView для отображения выбранного времени уведомлений
private TextView textSelectedTime;
// Кнопка для настройки времени уведомлений
private Button buttonSetTime;
private static final int REQUEST_NOTIFICATION_PERMISSION = 1001;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_currency_list);
// Инициализация репозитория
repository = new CurrencyRepository(this);
// Получение избранных пар валют из SharedPreferences
favoriteCurrencyPairs = getFavoriteCurrencyPairs();
// Инициализация RecyclerView и установка менеджера компоновки
recyclerView = findViewById(R.id.currency_recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
// Ссылка на TextView для пустого состояния
textEmpty = findViewById(R.id.text_empty);
// Ссылка на TextView для отображения выбранного времени
textSelectedTime = findViewById(R.id.text_selected_time);
// Инициализация адаптера с списком избранных пар и установка слушателя
currencyAdapter = new CurrencyAdapter(new ArrayList<>(favoriteCurrencyPairs), new CurrencyAdapter.OnFavoriteClickListener() {
@Override
public void onFavoriteClick(String currencyPair, boolean isFavorite) {
// Обработка нажатия на кнопку избранного
if (isFavorite) {
// Добавление в избранное
favoriteCurrencyPairs.add(currencyPair);
Toast.makeText(CurrencyListActivity.this, "Добавлено в избранное: " + currencyPair, Toast.LENGTH_SHORT).show();
} else {
// Удаление из избранного
favoriteCurrencyPairs.remove(currencyPair);
Toast.makeText(CurrencyListActivity.this, "Удалено из избранного: " + currencyPair, Toast.LENGTH_SHORT).show();
}
// Сохранение обновленного набора избранных пар
saveFavoriteCurrencyPairs(favoriteCurrencyPairs);
// Обновление интерфейса
updateUI();
}
});
// Установка адаптера в RecyclerView
recyclerView.setAdapter(currencyAdapter);
// Инициализация кнопки для настройки времени уведомлений
buttonSetTime = findViewById(R.id.button_set_notification_time);
// Получение сохраненного времени уведомлений
SharedPreferences prefs = getSharedPreferences("preferences", MODE_PRIVATE);
int savedHour = prefs.getInt("notification_hour", -1);
int savedMinute = prefs.getInt("notification_minute", -1);
if (savedHour != -1 && savedMinute != -1) {
String time = String.format("%02d:%02d", savedHour, savedMinute);
textSelectedTime.setText("Время уведомлений: " + time);
} else {
textSelectedTime.setText("Время уведомлений: не установлено");
}
// Проверка и запрос разрешения на отправку уведомлений для Android 13 и выше
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.POST_NOTIFICATIONS}, REQUEST_NOTIFICATION_PERMISSION);
}
}
// Установка слушателя для кнопки настройки времени уведомлений
buttonSetTime.setOnClickListener(v -> showTimePickerDialog());
// Инициализация и установка слушателя для кнопки "Назад"
Button backButton = findViewById(R.id.button_back);
backButton.setOnClickListener(v -> finish());
// Инициализация и установка слушателя для кнопки "Добавить пару"
Button addPairButton = findViewById(R.id.button_add_pair);
addPairButton.setOnClickListener(v -> new LoadCurrenciesTaskForDialog().execute());
// Обновление интерфейса в зависимости от наличия избранных пар
updateUI();
}
/**
* Обработка результата запроса разрешений.
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if(requestCode == REQUEST_NOTIFICATION_PERMISSION) {
if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
Toast.makeText(this, "Разрешение на уведомления предоставлено", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "Разрешение на уведомления отклонено. Уведомления не будут работать.", Toast.LENGTH_LONG).show();
// Отключение уведомлений, если разрешение не предоставлено
textSelectedTime.setText("Время уведомлений: не установлено");
CurrencyAlarmHelper.cancelAllNotifications(this);
}
}
}
/**
* Получает набор избранных пар валют из SharedPreferences.
*
* @return Новый HashSet с избранными пар.
*/
private Set<String> getFavoriteCurrencyPairs() {
SharedPreferences prefs = getSharedPreferences("favorites", MODE_PRIVATE);
Set<String> defaultSet = new HashSet<>();
Set<String> storedSet = prefs.getStringSet("favorite_pairs", defaultSet);
// Возвращаем новый HashSet, чтобы избежать изменения оригинального набора из SharedPreferences
return new HashSet<>(storedSet);
}
/**
* Сохраняет обновленный набор избранных пар валют в SharedPreferences.
*
* @param favorites Набор избранных пар валют для сохранения.
*/
private void saveFavoriteCurrencyPairs(Set<String> favorites) {
SharedPreferences prefs = getSharedPreferences("favorites", MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putStringSet("favorite_pairs", favorites);
editor.apply();
}
/**
* Обновляет интерфейс, переключая видимость RecyclerView и TextView для пустого состояния.
*/
private void updateUI() {
if (favoriteCurrencyPairs.isEmpty()) {
// Если нет избранных пар, показать сообщение и скрыть RecyclerView
recyclerView.setVisibility(View.GONE);
textEmpty.setVisibility(View.VISIBLE);
} else {
// Если есть избранные пары, показать RecyclerView и скрыть сообщение
recyclerView.setVisibility(View.VISIBLE);
textEmpty.setVisibility(View.GONE);
}
}
/**
* Показывает диалог выбора времени уведомлений.
*/
private void showTimePickerDialog() {
// Получение сохраненного времени
SharedPreferences prefs = getSharedPreferences("preferences", MODE_PRIVATE);
int savedHour = prefs.getInt("notification_hour", 8); // По умолчанию 08:00
int savedMinute = prefs.getInt("notification_minute", 0);
// Создание и отображение TimePickerDialog
TimePickerDialog timePickerDialog = new TimePickerDialog(this, (view, hourOfDay, minute) -> {
// Сохранение выбранного времени
saveNotificationTime(hourOfDay, minute);
String time = String.format("%02d:%02d", hourOfDay, minute);
textSelectedTime.setText("Время уведомлений: " + time);
// Настройка уведомлений с новым временем
CurrencyAlarmHelper.scheduleDailyNotification(this, hourOfDay, minute);
Toast.makeText(this, "Уведомления будут отправляться в " + time, Toast.LENGTH_SHORT).show();
}, savedHour, savedMinute, true); // true для 24-часового формата
timePickerDialog.setTitle("Выберите время уведомлений");
timePickerDialog.show();
}
/**
* Сохраняет выбранное время уведомлений в SharedPreferences.
*
* @param hour Выбранный час.
* @param minute Выбранные минуты.
*/
private void saveNotificationTime(int hour, int minute) {
SharedPreferences prefs = getSharedPreferences("preferences", MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putInt("notification_hour", hour);
editor.putInt("notification_minute", minute);
editor.apply();
}
/**
* Отменяет все уведомления и будильники.
*/
private void cancelAllNotifications() {
CurrencyAlarmHelper.cancelAllNotifications(this);
}
/**
* AsyncTask для загрузки списка валют в фоновом режиме для диалога добавления пары.
*/
private class LoadCurrenciesTaskForDialog extends AsyncTask<Void, Void, List<String>> {
@Override
protected List<String> doInBackground(Void... voids) {
// Получение всех уникальных валют из репозитория
return repository.getAllUniqueCurrencies();
}
@Override
protected void onPostExecute(List<String> currencies) {
if (currencies != null && !currencies.isEmpty()) {
// Если валюты доступны, показать диалог добавления пары
showAddPairDialog(currencies);
} else {
// Если валюты недоступны, уведомить пользователя
Toast.makeText(CurrencyListActivity.this, "Нет доступных валют для добавления.", Toast.LENGTH_SHORT).show();
}
}
}
/**
* Показывает AlertDialog, позволяющее пользователю выбрать и добавить новую пару валют.
*
* @param currencies Список доступных кодов валют.
*/
private void showAddPairDialog(List<String> currencies) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Добавить валютную пару");
// Раздувание пользовательского макета диалога
LayoutInflater inflater = LayoutInflater.from(this);
View dialogView = inflater.inflate(R.layout.dialog_add_pair, null);
Spinner spinnerBase = dialogView.findViewById(R.id.spinner_base);
Spinner spinnerTarget = dialogView.findViewById(R.id.spinner_target);
// Настройка адаптеров для Spinner
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, currencies);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerBase.setAdapter(adapter);
spinnerTarget.setAdapter(adapter);
// Установка пользовательского вида в диалог
builder.setView(dialogView);
// Определение действия для положительной кнопки (Добавить)
builder.setPositiveButton("Добавить", (dialog, which) -> {
String base = spinnerBase.getSelectedItem().toString();
String target = spinnerTarget.getSelectedItem().toString();
// Проверка, что базовая и целевая валюты не совпадают
if (base.equals(target)) {
Toast.makeText(this, "Базовая и целевая валюты не могут быть одинаковыми.", Toast.LENGTH_SHORT).show();
return;
}
String pair = base + "/" + target;
// Проверка, существует ли уже пара в избранном
if (favoriteCurrencyPairs.contains(pair)) {
Toast.makeText(this, "Эта пара уже добавлена в избранное.", Toast.LENGTH_SHORT).show();
return;
}
// Добавление новой пары в избранное
favoriteCurrencyPairs.add(pair);
saveFavoriteCurrencyPairs(favoriteCurrencyPairs);
// Обновление адаптера с новой парой
currencyAdapter.addCurrencyPair(pair);
// Уведомление пользователя
Toast.makeText(this, "Добавлено в избранное: " + pair, Toast.LENGTH_SHORT).show();
// Обновление интерфейса
updateUI();
});
// Определение действия для отрицательной кнопки (Отмена)
builder.setNegativeButton("Отмена", (dialog, which) -> dialog.dismiss());
// Показ диалога
builder.show();
}
}// CurrencyPair.java
package com.example.myapplication;
import java.util.Objects;
public class CurrencyPair {
private String base;
private String target;
public CurrencyPair(String base, String target) {
this.base = base;
this.target = target;
}
public String getBase() {
return base;
}
public String getTarget() {
return target;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CurrencyPair that = (CurrencyPair) o;
return Objects.equals(base, that.base) && Objects.equals(target, that.target);
}
@Override
public int hashCode() {
return Objects.hash(base, target);
}
@Override
public String toString() {
return base + "/" + target;
}
}package com.example.myapplication;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
import androidx.room.TypeConverters;
import java.util.Date;
@Entity(tableName = "currency_rate")
public class CurrencyRate {
@PrimaryKey(autoGenerate = true)
public int id;
public String baseCurrency;
public String targetCurrency;
public double rate;
@TypeConverters({Converters.class})
public Date date;
public CurrencyRate(String baseCurrency, String targetCurrency, double rate, Date date) {
this.baseCurrency = baseCurrency;
this.targetCurrency = targetCurrency;
this.rate = rate;
this.date = date;
}
@Override
public String toString() {
return baseCurrency + " -> " + targetCurrency + " = " + rate + " на " + date.toString();
}
}package com.example.myapplication;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import java.util.Date;
import java.util.List;
@Dao
public interface CurrencyRateDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertCurrencyRate(CurrencyRate rate);
/**
* Пакетная вставка курсов валют.
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertCurrencyRates(List<CurrencyRate> rates);
@Query("DELETE FROM currency_rate")
void clearAllRates();
@Query("SELECT DISTINCT baseCurrency FROM currency_rate")
List<String> getAllBaseCurrencies();
@Query("SELECT DISTINCT targetCurrency FROM currency_rate")
List<String> getAllTargetCurrencies();
@Query("SELECT DISTINCT date FROM currency_rate WHERE date BETWEEN :startDate AND :endDate ORDER BY date DESC")
List<Date> getAllDatesWithinRange(Date startDate, Date endDate);
@Query("SELECT * FROM currency_rate WHERE baseCurrency = :baseCurrency AND targetCurrency = :targetCurrency AND date BETWEEN :startDate AND :endDate ORDER BY date DESC")
List<CurrencyRate> getCurrencyRateHistoryWithinDates(String baseCurrency, String targetCurrency, Date startDate, Date endDate);
@Query("SELECT rate FROM currency_rate WHERE baseCurrency = :fromCurrency AND targetCurrency = :toCurrency ORDER BY date DESC LIMIT 1")
Double getLatestExchangeRate(String fromCurrency, String toCurrency);
@Query("SELECT DISTINCT date FROM currency_rate ORDER BY date DESC")
List<Date> getAllDates();
}package com.example.myapplication;
import android.content.Context;
import android.util.Log;
import com.example.myapplication.api.LoadCurrencyRatesTask;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class CurrencyRepository {
private final CurrencyRateDao currencyRateDao;
private final Context context;
public CurrencyRepository(Context context) {
this.context = context;
AppDatabase db = AppDatabase.getDatabase(context);
currencyRateDao = db.currencyRateDao();
}
public void insertCurrencyRateSync(CurrencyRate rate) {
currencyRateDao.insertCurrencyRate(rate);
Log.d("CurrencyRepository", "Вставлен курс: " + rate.toString());
}
/**
* Пакетная вставка курсов валют.
*/
public void insertCurrencyRatesSync(List<CurrencyRate> rates) {
currencyRateDao.insertCurrencyRates(rates);
Log.d("CurrencyRepository", "Вставлены курсы валют: " + rates.size());
}
public void clearAllRates() {
currencyRateDao.clearAllRates();
Log.d("CurrencyRepository", "Все курсы очищены из базы данных.");
}
public List<Date> getAllDates() {
return currencyRateDao.getAllDates();
}
// Метод для получения дат в заданном диапазоне
public List<Date> getAllDatesWithinRange(Date startDate, Date endDate) {
List<Date> dates = currencyRateDao.getAllDatesWithinRange(startDate, endDate);
Log.d("CurrencyRepository", "Получены даты в диапазоне: " + dates.toString());
return dates;
}
// Метод для получения исторических данных курса за заданный период
public List<CurrencyRate> getCurrencyRateHistoryWithinDates(String baseCurrency, String targetCurrency, Date startDate, Date endDate) {
List<CurrencyRate> rates = currencyRateDao.getCurrencyRateHistoryWithinDates(baseCurrency, targetCurrency, startDate, endDate);
Log.d("CurrencyRepository", "Получены курсы для " + baseCurrency + "/" + targetCurrency + " в диапазоне: " + rates.toString());
return rates;
}
public List<String> getAllUniqueCurrencies() {
List<String> baseCurrencies = currencyRateDao.getAllBaseCurrencies();
List<String> targetCurrencies = currencyRateDao.getAllTargetCurrencies();
Set<String> allCurrencies = new HashSet<>(baseCurrencies);
allCurrencies.addAll(targetCurrencies);
List<String> uniqueCurrencies = new ArrayList<>(allCurrencies);
Log.d("CurrencyRepository", "Уникальные валюты: " + uniqueCurrencies.toString());
return uniqueCurrencies;
}
public Double getExchangeRate(String fromCurrency, String toCurrency) {
Double rate = currencyRateDao.getLatestExchangeRate(fromCurrency, toCurrency);
Log.d("CurrencyRepository", "Последний курс с " + fromCurrency + " на " + toCurrency + ": " + rate);
return rate;
}
public void loadAllCurrencyRatesFromApi(LoadCurrencyRatesTask.Callback callback) {
new LoadCurrencyRatesTask(context, this, callback).execute();
}
public List<CurrencyPair> getAllUniqueCurrencyPairs() {
List<String> currencies = getAllUniqueCurrencies();
List<CurrencyPair> pairs = new ArrayList<>();
for (int i = 0; i < currencies.size(); i++) {
for (int j = i + 1; j < currencies.size(); j++) {
pairs.add(new CurrencyPair(currencies.get(i), currencies.get(j)));
pairs.add(new CurrencyPair(currencies.get(j), currencies.get(i)));
}
}
return pairs;
}
}package com.example.myapplication;
import android.content.Context;
import android.widget.TextView;
import com.github.mikephil.charting.components.MarkerView;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.highlight.Highlight;
import com.example.myapplication.R;
import com.github.mikephil.charting.utils.MPPointF;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
public class CustomMarkerView extends MarkerView {
private final TextView tvContent;
private final List<Date> dates;
private MPPointF mOffset;
public CustomMarkerView(Context context, int layoutResource, List<Date> dates) {
super(context, layoutResource);
this.dates = dates;
tvContent = findViewById(R.id.tvContent);
}
@Override
public void refreshContent(Entry e, Highlight highlight) {
int index = (int) e.getX();
if (index >= 0 && index < dates.size()) {
Date date = dates.get(index);
SimpleDateFormat dateFormat = new SimpleDateFormat("dd MMM yyyy", Locale.getDefault());
String dateStr = dateFormat.format(date);
String rateStr = String.format(Locale.getDefault(), "%.4f", e.getY());
tvContent.setText(dateStr + "\nКурс: " + rateStr);
} else {
tvContent.setText("");
}
super.refreshContent(e, highlight);
}
@Override
public MPPointF getOffset() {
if (mOffset == null) {
mOffset = new MPPointF(-(getWidth() / 2f), -getHeight());
}
return mOffset;
}
}package com.example.myapplication;
import android.app.NotificationChannel;
import android.app.NotificationManager;
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 java.util.Set;
/**
* BroadcastReceiver, который обрабатывает ежедневные уведомления по избранным курсам валют.
*/
public class DailyNotificationReceiver extends BroadcastReceiver {
private static final String CHANNEL_ID = "daily_rate_channel";
private static final String NOTIFICATION_TITLE = "Курсы избранных валют";
private static final int NOTIFICATION_ID = 1;
@Override
public void onReceive(Context context, Intent intent) {
try {
// Доступ к SharedPreferences для получения избранных пар валют и времени уведомлений
SharedPreferences prefs = context.getSharedPreferences("preferences", Context.MODE_PRIVATE);
Set<String> favoritePairs = prefs.getStringSet("favorite_pairs", null);
int notificationHour = prefs.getInt("notification_hour", -1);
int notificationMinute = prefs.getInt("notification_minute", -1);
// Продолжаем только если есть избранные пары и время уведомлений установлено
if (favoritePairs != null && !favoritePairs.isEmpty() && notificationHour != -1 && notificationMinute != -1) {
StringBuilder contentBuilder = new StringBuilder();
// Создаём экземпляр CurrencyRepository для получения курсов валют
CurrencyRepository repository = new CurrencyRepository(context);
for (String pair : favoritePairs) {
String[] currencies = pair.split("/");
if (currencies.length == 2) {
String fromCurrency = currencies[0];
String toCurrency = currencies[1];
Double rate = repository.getExchangeRate(fromCurrency, toCurrency);
if (rate != null) {
contentBuilder.append(String.format("%s/%s: %.4f\n", fromCurrency, toCurrency, rate));
} else {
contentBuilder.append(String.format("%s/%s: нет данных\n", fromCurrency, toCurrency));
}
} else {
contentBuilder.append(String.format("%s: неверный формат пары\n", pair));
}
}
String contentText = contentBuilder.toString();
// Создание Notification Channel для Android O и выше
createNotificationChannel(context);
// Построение уведомления
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.star) // Убедитесь, что иконка существует
.setContentTitle(NOTIFICATION_TITLE)
.setContentText("Курсы валют на сегодня")
.setStyle(new NotificationCompat.BigTextStyle().bigText(contentText))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setAutoCancel(true);
// Получение NotificationManager для отображения уведомления
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (notificationManager != null) {
notificationManager.notify(NOTIFICATION_ID, builder.build());
}
// Перенастройка следующего уведомления
CurrencyAlarmHelper.scheduleDailyNotification(context, notificationHour, notificationMinute);
}
} catch (Exception e) {
e.printStackTrace();
// Здесь можно добавить логирование или другие действия по обработке исключений
}
}
/**
* Создаёт Notification Channel для отправки уведомлений.
*
* @param context Контекст приложения.
*/
private void createNotificationChannel(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = "Daily Rate Channel";
String description = "Channel for daily exchange rate notifications";
int importance = NotificationManager.IMPORTANCE_DEFAULT;
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
channel.setDescription(description);
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
if (notificationManager != null) {
notificationManager.createNotificationChannel(channel);
}
}
}
}package com.example.myapplication;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log; // Добавлено для логирования
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.Spinner;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.example.myapplication.api.LoadCurrencyRatesTask;
import com.google.android.material.button.MaterialButton;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private Spinner spinnerFrom;
private Spinner spinnerTo;
private EditText inputFrom;
private EditText inputTo;
private ProgressBar loadingIndicator;
private CurrencyRepository repository;
private MaterialButton buttonSwap;
private MaterialButton buttonShowChart; // Кнопка для перехода к графику
private Button buttonCurrencyList;
private boolean isRatesLoaded = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Инициализация компонентов
repository = new CurrencyRepository(this);
spinnerFrom = findViewById(R.id.spinner_from);
spinnerTo = findViewById(R.id.spinner_to);
inputFrom = findViewById(R.id.input_from);
inputTo = findViewById(R.id.input_to);
loadingIndicator = findViewById(R.id.loading_indicator);
buttonSwap = findViewById(R.id.button_swap);
buttonShowChart = findViewById(R.id.button_show_chart); // Кнопка для показа графика
buttonCurrencyList = findViewById(R.id.button_currency_list); // Инициализация кнопки избранных валют
// Настройка обработчика кнопки избранных валют
buttonCurrencyList.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this, CurrencyListActivity.class);
startActivity(intent);
}
});
// Настройка обработчика кнопки перехода к графику
buttonShowChart.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (isRatesLoaded) {
Intent intent = new Intent(MainActivity.this, ChartActivity.class);
String selectedFromCurrency = spinnerFrom.getSelectedItem().toString();
String selectedToCurrency = spinnerTo.getSelectedItem().toString();
intent.putExtra("FROM_CURRENCY", selectedFromCurrency);
intent.putExtra("TO_CURRENCY", selectedToCurrency);
startActivity(intent);
} else {
showToast("Подождите, пока загрузятся курсы валют.");
}
}
});
// Настройка обработчика кнопки свапа
buttonSwap.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int fromPosition = spinnerFrom.getSelectedItemPosition();
int toPosition = spinnerTo.getSelectedItemPosition();
spinnerFrom.setSelection(toPosition);
spinnerTo.setSelection(fromPosition);
// Обмен значениями в полях ввода (опционально)
String inputFromValue = inputFrom.getText().toString();
String inputToValue = inputTo.getText().toString();
inputFrom.setText(inputToValue);
inputTo.setText(inputFromValue);
if (isRatesLoaded) {
performConversion();
} else {
showToast("Подождите, пока загрузятся курсы валют.");
}
}
});
// Показываем индикатор загрузки и загружаем курсы
loadingIndicator.setVisibility(View.VISIBLE);
repository.loadAllCurrencyRatesFromApi(new LoadCurrencyRatesTask.Callback() {
@Override
public void onRatesLoaded() {
runOnUiThread(() -> {
new LoadCurrenciesTask().execute();
});
}
@Override
public void onError(Exception e) {
runOnUiThread(() -> {
// Не очищаем базу данных, используем кэшированные данные
loadingIndicator.setVisibility(View.GONE);
showToast("Ошибка обновления курсов валют: " + e.getMessage());
Log.e("MainActivity", "Error updating currency rates", e);
// Теперь загружаем данные из кэша
new LoadCurrenciesTask().execute();
});
}
});
inputFrom.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
if (isRatesLoaded) {
performConversion();
} else {
showToast("Подождите, пока загрузятся курсы валют.");
}
}
});
AdapterView.OnItemSelectedListener spinnerListener = new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (isRatesLoaded) {
performConversion();
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {}
};
spinnerFrom.setOnItemSelectedListener(spinnerListener);
spinnerTo.setOnItemSelectedListener(spinnerListener);
}
private class LoadCurrenciesTask extends AsyncTask<Void, Void, List<String>> {
@Override
protected void onPreExecute() {
loadingIndicator.setVisibility(View.VISIBLE);
}
@Override
protected List<String> doInBackground(Void... voids) {
List<String> currencies = repository.getAllUniqueCurrencies();
Log.d("LoadCurrenciesTask", "Получены валюты: " + currencies.toString());
return currencies;
}
@Override
protected void onPostExecute(List<String> currencies) {
super.onPostExecute(currencies);
loadingIndicator.setVisibility(View.GONE);
Log.d("LoadCurrenciesTask", "onPostExecute с валютами: " + currencies);
if (currencies != null && !currencies.isEmpty()) {
isRatesLoaded = true;
ArrayAdapter<String> adapterFrom = new ArrayAdapter<>(MainActivity.this, android.R.layout.simple_spinner_item, currencies);
adapterFrom.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerFrom.setAdapter(adapterFrom);
ArrayAdapter<String> adapterTo = new ArrayAdapter<>(MainActivity.this, android.R.layout.simple_spinner_item, currencies);
adapterTo.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerTo.setAdapter(adapterTo);
// Устанавливаем стандартные значения
spinnerFrom.setSelection(0);
spinnerTo.setSelection(1);
// Выполняем конвертацию
performConversion();
} else {
if (!isInternetAvailable()) { // Метод для проверки сети
showToast("Нет интернет-соединения и нет кэшированных данных.");
} else {
showToast("Не удалось загрузить валюты.");
}
Log.e("LoadCurrenciesTask", "Список валют пуст");
}
}
}
private void performConversion() {
String fromCurrency = spinnerFrom.getSelectedItem().toString().toUpperCase();
String toCurrency = spinnerTo.getSelectedItem().toString().toUpperCase();
String amountStr = inputFrom.getText().toString();
if (amountStr.isEmpty()) {
inputTo.setText("");
return;
}
if (fromCurrency.equals(toCurrency)) {
inputTo.setText(amountStr);
return;
}
try {
double amount = Double.parseDouble(amountStr);
// Выполняем конвертацию
new ConvertCurrencyTask(fromCurrency, toCurrency, amount, repository, result -> {
if (result != null) {
inputTo.setText(String.format("%.2f", result));
} else {
showToast("Ошибка при конвертации валюты: Курс не найден");
inputTo.setText("");
}
}).execute();
} catch (NumberFormatException e) {
inputTo.setText("");
}
}
private boolean isInternetAvailable() {
ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
if (connectivityManager != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Network network = connectivityManager.getActiveNetwork();
if (network == null) return false;
NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network);
return capabilities != null &&
(capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET));
} else {
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
return networkInfo != null && networkInfo.isConnected();
}
}
return false;
}
private void showToast(String message) {
Toast.makeText(MainActivity.this, message, Toast.LENGTH_SHORT).show();
}
}<?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"
android:id="@+id/chart_constraint_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:background="@color/background_color">
<!-- Кнопка "Назад" -->
<com.google.android.material.button.MaterialButton
android:id="@+id/button_back_to_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Назад"
app:cornerRadius="8dp"
android:layout_marginBottom="16dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<!-- Спиннеры для выбора валют -->
<Spinner
android:id="@+id/spinner_from_chart"
style="@style/SpinnerStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
app:layout_constraintTop_toBottomOf="@id/button_back_to_main"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/button_swap_chart" />
<!-- Кнопка для свапа валют -->
<com.google.android.material.button.MaterialButton
android:id="@+id/button_swap_chart"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
app:cornerRadius="24dp"
app:backgroundTint="@color/colorPrimary"
app:icon="@drawable/baseline_swap_horiz"
app:iconTint="@android:color/white"
app:iconGravity="start"
app:rippleColor="@color/colorPrimaryDark"
android:elevation="4dp"
android:contentDescription="Поменять местами валюты"
app:layout_constraintTop_toTopOf="@id/spinner_from_chart"
app:layout_constraintStart_toEndOf="@id/spinner_from_chart"
app:layout_constraintEnd_toStartOf="@id/spinner_to_chart" />
<!-- Второй спиннер -->
<Spinner
android:id="@+id/spinner_to_chart"
style="@style/SpinnerStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="@id/spinner_from_chart"
app:layout_constraintStart_toEndOf="@id/button_swap_chart"
app:layout_constraintEnd_toEndOf="parent" />
<!-- Layout для выбора периода -->
<LinearLayout
android:id="@+id/time_range_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@id/spinner_from_chart"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<TextView
android:id="@+id/time_range_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Период:"
android:textSize="16sp"
android:layout_marginEnd="8dp"
android:textColor="@color/black" />
<!-- Группа переключателей периода -->
<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/time_range_toggle_group_chart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:singleSelection="true">
<com.google.android.material.button.MaterialButton
android:id="@+id/button_7_days_chart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="7 дней"
android:checked="true"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_30_days_chart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="30 дней"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_90_days_chart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="90 дней"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
</com.google.android.material.button.MaterialButtonToggleGroup>
</LinearLayout>
<!-- График LineChart -->
<com.github.mikephil.charting.charts.LineChart
android:id="@+id/line_chart"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@android:color/white"
android:elevation="4dp"
android:layout_marginTop="24dp"
android:layout_marginBottom="16dp"
app:layout_constraintTop_toBottomOf="@id/time_range_layout"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout><?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:id="@+id/constraint_layout_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:background="@color/background_color"
tools:context=".MainActivity">
<!-- Загрузочный индикатор (ProgressBar) -->
<ProgressBar
android:id="@+id/loading_indicator"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminateTint="@color/colorPrimary"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- Спиннеры выбора валют -->
<Spinner
android:id="@+id/spinner_from"
style="@style/SpinnerStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toStartOf="@+id/button_swap"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- Кнопка для свапа валют с улучшенной стилизацией -->
<com.google.android.material.button.MaterialButton
android:id="@+id/button_swap"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
app:cornerRadius="24dp"
app:backgroundTint="@color/colorPrimary"
app:icon="@drawable/baseline_swap_horiz"
app:iconTint="@android:color/white"
app:iconGravity="start"
app:rippleColor="@color/colorPrimaryDark"
android:elevation="4dp"
android:contentDescription="Поменять местами валюты"
app:layout_constraintStart_toEndOf="@id/spinner_from"
app:layout_constraintEnd_toStartOf="@id/spinner_to"
app:layout_constraintTop_toTopOf="@id/spinner_from" />
<Spinner
android:id="@+id/spinner_to"
style="@style/SpinnerStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toEndOf="@id/button_swap"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/spinner_from" />
<!-- Поле ввода суммы -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_layout_from"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="Введите сумму"
app:boxStrokeColor="@color/colorPrimary"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/spinner_from">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_from"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberDecimal"
android:padding="12dp" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Поле вывода конвертированной суммы -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_layout_to"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="Конвертированная сумма"
app:boxStrokeColor="@color/colorPrimary"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/input_layout_from">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_to"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:focusable="false"
android:padding="12dp"
android:background="@drawable/edittext_background_disabled" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Кнопка для перехода к графику -->
<com.google.android.material.button.MaterialButton
android:id="@+id/button_show_chart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Показать график"
app:cornerRadius="8dp"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@id/input_layout_to"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<!-- Кнопка "Избранные валюты" -->
<com.google.android.material.button.MaterialButton
android:id="@+id/button_currency_list"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Избранные валюты"
app:cornerRadius="8dp"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@id/button_show_chart"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> нужно добавть в активностях дату текущего курса