mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-29 05:16:51 +01:00
Zepp OS: Add loyalty cards integration with Catima
This commit is contained in:
parent
e95c8a3775
commit
fea3bf50a4
@ -26,6 +26,9 @@
|
||||
<uses-permission android:name="android.permission.SEND_SMS" />
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||
|
||||
<!-- Read loyalty cards from Catima -->
|
||||
<uses-permission android:name="me.hackerchick.catima.READ_CARDS"/>
|
||||
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_CALENDAR" />
|
||||
<uses-permission
|
||||
@ -107,6 +110,10 @@
|
||||
android:name=".devices.miband.MiBandPreferencesActivity"
|
||||
android:label="@string/preferences_miband_settings"
|
||||
android:parentActivityName=".activities.SettingsActivity" />
|
||||
<activity
|
||||
android:name=".activities.loyaltycards.LoyaltyCardsSettingsActivity"
|
||||
android:label="@string/loyalty_cards"
|
||||
android:parentActivityName=".activities.devicesettings.DeviceSettingsActivity" />
|
||||
<activity
|
||||
android:name=".devices.zetime.ZeTimePreferenceActivity"
|
||||
android:label="@string/zetime_title_settings"
|
||||
|
@ -17,6 +17,7 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.devicesettings;
|
||||
|
||||
public class DeviceSettingsPreferenceConst {
|
||||
public static final String PREF_HEADER_APPS = "pref_header_apps";
|
||||
public static final String PREF_HEADER_TIME = "pref_header_time";
|
||||
public static final String PREF_HEADER_DISPLAY = "pref_header_display";
|
||||
public static final String PREF_HEADER_HEALTH = "pref_header_health";
|
||||
|
@ -52,6 +52,8 @@ import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.CalBlacklistActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureContacts;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureWorldClocks;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
@ -919,6 +921,16 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat imp
|
||||
}
|
||||
}
|
||||
|
||||
final Preference loyaltyCards = findPreference(LoyaltyCardsSettingsConst.PREF_KEY_LOYALTY_CARDS);
|
||||
if (loyaltyCards != null) {
|
||||
loyaltyCards.setOnPreferenceClickListener(preference -> {
|
||||
final Intent intent = new Intent(getContext(), LoyaltyCardsSettingsActivity.class);
|
||||
intent.putExtra(GBDevice.EXTRA_DEVICE, getDevice());
|
||||
startActivity(intent);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
if (deviceSpecificSettingsCustomizer != null) {
|
||||
deviceSpecificSettingsCustomizer.customizeSettings(this, prefs);
|
||||
}
|
||||
|
@ -0,0 +1,118 @@
|
||||
/* Copyright (C) 2023 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards;
|
||||
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
|
||||
|
||||
public class LoyaltyCardsSettingsActivity extends AbstractGBActivity implements
|
||||
PreferenceFragmentCompat.OnPreferenceStartScreenCallback,
|
||||
ActivityCompat.OnRequestPermissionsResultCallback {
|
||||
|
||||
public static final int PERMISSION_REQUEST_CODE = 0;
|
||||
|
||||
private GBDevice device;
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
device = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_loyalty_cards);
|
||||
if (savedInstanceState == null) {
|
||||
Fragment fragment = getSupportFragmentManager().findFragmentByTag(LoyaltyCardsSettingsFragment.FRAGMENT_TAG);
|
||||
if (fragment == null) {
|
||||
fragment = LoyaltyCardsSettingsFragment.newInstance(device);
|
||||
}
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings_container, fragment, LoyaltyCardsSettingsFragment.FRAGMENT_TAG)
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceStartScreen(final PreferenceFragmentCompat caller, final PreferenceScreen preferenceScreen) {
|
||||
final PreferenceFragmentCompat fragment = LoyaltyCardsSettingsFragment.newInstance(device);
|
||||
final Bundle args = fragment.getArguments();
|
||||
if (args != null) {
|
||||
args.putString(PreferenceFragmentCompat.ARG_PREFERENCE_ROOT, preferenceScreen.getKey());
|
||||
fragment.setArguments(args);
|
||||
}
|
||||
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings_container, fragment, preferenceScreen.getKey())
|
||||
.addToBackStack(preferenceScreen.getKey())
|
||||
.commit();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
// Simulate a back press, so that we don't actually exit the activity when
|
||||
// in a nested PreferenceScreen
|
||||
this.onBackPressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(final int requestCode,
|
||||
@NonNull final String[] permissions,
|
||||
@NonNull final int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
|
||||
if (requestCode != PERMISSION_REQUEST_CODE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (grantResults.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
final FragmentManager fragmentManager = getSupportFragmentManager();
|
||||
final Fragment fragment = fragmentManager.findFragmentByTag(LoyaltyCardsSettingsFragment.FRAGMENT_TAG);
|
||||
if (fragment == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fragment instanceof LoyaltyCardsSettingsFragment) {
|
||||
((LoyaltyCardsSettingsFragment) fragment).reloadPreferences(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
/* Copyright (C) 2022 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards;
|
||||
|
||||
public final class LoyaltyCardsSettingsConst {
|
||||
public static final String PREF_KEY_LOYALTY_CARDS = "pref_key_loyalty_cards";
|
||||
|
||||
public static final String PREF_KEY_HEADER_LOYALTY_CARDS_CATIMA = "pref_key_header_loyalty_cards_catima";
|
||||
public static final String PREF_KEY_HEADER_LOYALTY_CARDS_SYNC = "pref_key_header_loyalty_cards_sync";
|
||||
public static final String PREF_KEY_HEADER_LOYALTY_CARDS_SYNC_OPTIONS = "pref_key_header_loyalty_cards_sync_options";
|
||||
|
||||
public static final String LOYALTY_CARDS_CATIMA_PACKAGE = "loyalty_cards_catima_package";
|
||||
public static final String LOYALTY_CARDS_OPEN_CATIMA = "loyalty_cards_open_catima";
|
||||
public static final String LOYALTY_CARDS_CATIMA_NOT_INSTALLED = "loyalty_cards_catima_not_installed";
|
||||
public static final String LOYALTY_CARDS_CATIMA_NOT_COMPATIBLE = "loyalty_cards_catima_not_compatible";
|
||||
public static final String LOYALTY_CARDS_INSTALL_CATIMA = "loyalty_cards_install_catima";
|
||||
public static final String LOYALTY_CARDS_CATIMA_PERMISSIONS = "loyalty_cards_catima_permissions";
|
||||
public static final String LOYALTY_CARDS_SYNC = "loyalty_cards_sync";
|
||||
public static final String LOYALTY_CARDS_SYNC_GROUPS_ONLY = "loyalty_cards_sync_groups_only";
|
||||
public static final String LOYALTY_CARDS_SYNC_GROUPS = "loyalty_cards_sync_groups";
|
||||
public static final String LOYALTY_CARDS_SYNC_ARCHIVED = "loyalty_cards_sync_archived";
|
||||
public static final String LOYALTY_CARDS_SYNC_STARRED = "loyalty_cards_sync_starred";
|
||||
}
|
@ -0,0 +1,260 @@
|
||||
/* Copyright (C) 2022 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsConst.LOYALTY_CARDS_CATIMA_NOT_COMPATIBLE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsConst.LOYALTY_CARDS_CATIMA_NOT_INSTALLED;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsConst.LOYALTY_CARDS_CATIMA_PACKAGE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsConst.LOYALTY_CARDS_CATIMA_PERMISSIONS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsConst.LOYALTY_CARDS_INSTALL_CATIMA;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsConst.LOYALTY_CARDS_OPEN_CATIMA;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsConst.LOYALTY_CARDS_SYNC;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsConst.LOYALTY_CARDS_SYNC_ARCHIVED;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsConst.LOYALTY_CARDS_SYNC_GROUPS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsConst.LOYALTY_CARDS_SYNC_GROUPS_ONLY;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsConst.LOYALTY_CARDS_SYNC_STARRED;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsConst.PREF_KEY_HEADER_LOYALTY_CARDS_CATIMA;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsConst.PREF_KEY_HEADER_LOYALTY_CARDS_SYNC;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsConst.PREF_KEY_HEADER_LOYALTY_CARDS_SYNC_OPTIONS;
|
||||
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.MultiSelectListPreference;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceCategory;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.SwitchPreference;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.loyaltycards.CatimaContentProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.loyaltycards.CatimaManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.loyaltycards.LoyaltyCard;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||
|
||||
public class LoyaltyCardsSettingsFragment extends PreferenceFragmentCompat {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(LoyaltyCardsSettingsFragment.class);
|
||||
|
||||
static final String FRAGMENT_TAG = "LOYALTY_CARDS_SETTINGS_FRAGMENT";
|
||||
|
||||
private GBDevice device;
|
||||
|
||||
private void setSettingsFileSuffix(final String settingsFileSuffix) {
|
||||
Bundle args = new Bundle();
|
||||
args.putString("settingsFileSuffix", settingsFileSuffix);
|
||||
setArguments(args);
|
||||
}
|
||||
|
||||
private void setDevice(final GBDevice device) {
|
||||
final Bundle args = getArguments() != null ? getArguments() : new Bundle();
|
||||
args.putParcelable("device", device);
|
||||
setArguments(args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
|
||||
final Bundle arguments = getArguments();
|
||||
if (arguments == null) {
|
||||
return;
|
||||
}
|
||||
final String settingsFileSuffix = arguments.getString("settingsFileSuffix", null);
|
||||
this.device = arguments.getParcelable("device");
|
||||
|
||||
if (settingsFileSuffix == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
getPreferenceManager().setSharedPreferencesName("devicesettings_" + settingsFileSuffix);
|
||||
setPreferencesFromResource(R.xml.loyalty_cards, rootKey);
|
||||
|
||||
reloadPreferences(null);
|
||||
}
|
||||
|
||||
static LoyaltyCardsSettingsFragment newInstance(GBDevice device) {
|
||||
final String settingsFileSuffix = device.getAddress();
|
||||
final LoyaltyCardsSettingsFragment fragment = new LoyaltyCardsSettingsFragment();
|
||||
fragment.setSettingsFileSuffix(settingsFileSuffix);
|
||||
fragment.setDevice(device);
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
protected void reloadPreferences(String catimaPackageName) {
|
||||
final CatimaManager catimaManager = new CatimaManager(requireContext());
|
||||
|
||||
final List<CharSequence> installedCatimaPackages = catimaManager.findInstalledCatimaPackages();
|
||||
final boolean catimaInstalled = !installedCatimaPackages.isEmpty();
|
||||
|
||||
final ListPreference catimaPackage = findPreference(LOYALTY_CARDS_CATIMA_PACKAGE);
|
||||
CatimaContentProvider catima = null;
|
||||
|
||||
if (catimaPackage != null) {
|
||||
catimaPackage.setEntries(installedCatimaPackages.toArray(new CharSequence[0]));
|
||||
catimaPackage.setEntryValues(installedCatimaPackages.toArray(new CharSequence[0]));
|
||||
catimaPackage.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
LOG.info("Catima package changed to {}", newValue);
|
||||
reloadPreferences((String) newValue);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (StringUtils.isNullOrEmpty(catimaPackage.getValue()) || !installedCatimaPackages.contains(catimaPackage.getValue())) {
|
||||
catimaPackage.setValue(installedCatimaPackages.get(0).toString());
|
||||
}
|
||||
|
||||
if (installedCatimaPackages.size() <= 1) {
|
||||
catimaPackage.setVisible(false);
|
||||
}
|
||||
|
||||
catima = new CatimaContentProvider(requireContext(), catimaPackageName != null ? catimaPackageName : catimaPackage.getValue());
|
||||
}
|
||||
|
||||
final Preference openCatima = findPreference(LOYALTY_CARDS_OPEN_CATIMA);
|
||||
if (openCatima != null) {
|
||||
openCatima.setVisible(catimaInstalled);
|
||||
openCatima.setOnPreferenceClickListener(preference -> {
|
||||
if (catimaPackage != null) {
|
||||
final PackageManager packageManager = requireContext().getPackageManager();
|
||||
final Intent launchIntent = packageManager.getLaunchIntentForPackage(catimaPackageName != null ? catimaPackageName : catimaPackage.getValue());
|
||||
if (launchIntent != null) {
|
||||
startActivity(launchIntent);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
final Preference catimaNotInstalled = findPreference(LOYALTY_CARDS_CATIMA_NOT_INSTALLED);
|
||||
if (catimaNotInstalled != null) {
|
||||
catimaNotInstalled.setVisible(!catimaInstalled);
|
||||
}
|
||||
|
||||
final Preference installCatima = findPreference(LOYALTY_CARDS_INSTALL_CATIMA);
|
||||
if (installCatima != null) {
|
||||
installCatima.setVisible(!catimaInstalled);
|
||||
installCatima.setOnPreferenceClickListener(preference -> {
|
||||
installCatima();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
final boolean permissionGranted = ContextCompat.checkSelfPermission(requireContext(), CatimaContentProvider.PERMISSION_READ_CARDS) == PackageManager.PERMISSION_GRANTED;
|
||||
if (catimaInstalled) {
|
||||
final Preference catimaPermissions = findPreference(LOYALTY_CARDS_CATIMA_PERMISSIONS);
|
||||
if (catimaPermissions != null) {
|
||||
catimaPermissions.setVisible(!permissionGranted);
|
||||
catimaPermissions.setOnPreferenceClickListener(preference -> {
|
||||
ActivityCompat.requestPermissions(
|
||||
requireActivity(),
|
||||
new String[]{CatimaContentProvider.PERMISSION_READ_CARDS},
|
||||
LoyaltyCardsSettingsActivity.PERMISSION_REQUEST_CODE
|
||||
);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
final boolean catimaCompatible = catima != null && catima.isCatimaCompatible();
|
||||
final Preference catimaNotCompatible = findPreference(LOYALTY_CARDS_CATIMA_NOT_COMPATIBLE);
|
||||
if (catimaNotCompatible != null) {
|
||||
catimaNotCompatible.setVisible(catimaInstalled && permissionGranted && !catimaCompatible);
|
||||
}
|
||||
|
||||
final Preference sync = findPreference(LOYALTY_CARDS_SYNC);
|
||||
if (sync != null) {
|
||||
sync.setEnabled(catimaInstalled);
|
||||
sync.setOnPreferenceClickListener(preference -> {
|
||||
catimaManager.sync(device);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
final PreferenceCategory headerCatima = findPreference(PREF_KEY_HEADER_LOYALTY_CARDS_CATIMA);
|
||||
if (headerCatima != null) {
|
||||
boolean allHidden = true;
|
||||
for (int i = 0; i < headerCatima.getPreferenceCount(); i++) {
|
||||
if (headerCatima.getPreference(i).isVisible()) {
|
||||
allHidden = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
headerCatima.setVisible(!allHidden);
|
||||
}
|
||||
|
||||
if (catimaInstalled && catimaCompatible && permissionGranted) {
|
||||
final MultiSelectListPreference syncGroups = findPreference(LOYALTY_CARDS_SYNC_GROUPS);
|
||||
if (syncGroups != null) {
|
||||
final List<String> groups = catima.getGroups();
|
||||
final CharSequence[] entries = groups.toArray(new CharSequence[0]);
|
||||
syncGroups.setEntries(entries);
|
||||
syncGroups.setEntryValues(entries);
|
||||
|
||||
// Remove groups that do not exist anymore from the summary
|
||||
final Set<String> values = new HashSet<>(syncGroups.getValues());
|
||||
final Set<String> toRemove = new HashSet<>();
|
||||
for (final String group : values) {
|
||||
if (!groups.contains(group)) {
|
||||
toRemove.add(group);
|
||||
}
|
||||
}
|
||||
values.removeAll(toRemove);
|
||||
syncGroups.setSummary(String.join(", ", values));
|
||||
}
|
||||
}
|
||||
|
||||
final boolean allowSync = catimaInstalled && permissionGranted && catimaCompatible;
|
||||
final PreferenceCategory syncCategory = findPreference(PREF_KEY_HEADER_LOYALTY_CARDS_SYNC);
|
||||
if (syncCategory != null) {
|
||||
for (int i = 0; i < syncCategory.getPreferenceCount(); i++) {
|
||||
syncCategory.getPreference(i).setEnabled(allowSync);
|
||||
}
|
||||
}
|
||||
final PreferenceCategory syncOptionsCategory = findPreference(PREF_KEY_HEADER_LOYALTY_CARDS_SYNC_OPTIONS);
|
||||
if (syncOptionsCategory != null) {
|
||||
for (int i = 0; i < syncOptionsCategory.getPreferenceCount(); i++) {
|
||||
syncOptionsCategory.getPreference(i).setEnabled(allowSync);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void installCatima() {
|
||||
try {
|
||||
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=me.hackerchick.catima")));
|
||||
} catch (final ActivityNotFoundException e) {
|
||||
GB.toast(requireContext(), requireContext().getString(R.string.loyalty_cards_install_catima_fail), Toast.LENGTH_LONG, GB.WARN);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
/* Copyright (C) 2023 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.capabilities.loyaltycards;
|
||||
|
||||
/**
|
||||
* Matching com.google.zxing.BarcodeFormat
|
||||
*/
|
||||
public enum BarcodeFormat {
|
||||
AZTEC,
|
||||
CODE_39,
|
||||
CODE_93,
|
||||
CODE_128,
|
||||
CODABAR,
|
||||
DATA_MATRIX,
|
||||
EAN_8,
|
||||
EAN_13,
|
||||
ITF,
|
||||
PDF_417,
|
||||
QR_CODE,
|
||||
UPC_A,
|
||||
UPC_E,
|
||||
}
|
@ -0,0 +1,260 @@
|
||||
/* Copyright (C) 2023 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.capabilities.loyaltycards;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Currency;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
public class CatimaContentProvider {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CatimaContentProvider.class);
|
||||
|
||||
public static final List<String> KNOWN_PACKAGES = new ArrayList<String>() {{
|
||||
add("me.hackerchick.catima");
|
||||
add("me.hackerchick.catima.debug");
|
||||
}};
|
||||
|
||||
public static final String PERMISSION_READ_CARDS = "me.hackerchick.catima.READ_CARDS";
|
||||
|
||||
private final Context mContext;
|
||||
private final Uri versionUri;
|
||||
private final Uri cardsUri;
|
||||
private final Uri groupsUri;
|
||||
private final Uri cardGroupsUri;
|
||||
|
||||
public CatimaContentProvider(final Context context, final String catimaPackageName) {
|
||||
this.mContext = context;
|
||||
final String catimaAuthority = catimaPackageName + ".contentprovider.cards";
|
||||
this.versionUri = Uri.parse(String.format(Locale.ROOT, "content://%s/version", catimaAuthority));
|
||||
this.cardsUri = Uri.parse(String.format(Locale.ROOT, "content://%s/cards", catimaAuthority));
|
||||
this.groupsUri = Uri.parse(String.format(Locale.ROOT, "content://%s/groups", catimaAuthority));
|
||||
this.cardGroupsUri = Uri.parse(String.format(Locale.ROOT, "content://%s/card_groups", catimaAuthority));
|
||||
}
|
||||
|
||||
public boolean isCatimaCompatible() {
|
||||
final ContentResolver contentResolver = mContext.getContentResolver();
|
||||
try (Cursor cursor = contentResolver.query(versionUri, null, null, null, null)) {
|
||||
if (cursor == null || cursor.getCount() == 0) {
|
||||
LOG.warn("Catima content provider version not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
cursor.moveToNext();
|
||||
final int major = cursor.getInt(cursor.getColumnIndexOrThrow("major"));
|
||||
final int minor = cursor.getInt(cursor.getColumnIndexOrThrow("minor"));
|
||||
|
||||
LOG.info("Got catima content provider version: {}.{}", major, minor);
|
||||
|
||||
// We only support version 1.x for now
|
||||
return major == 1;
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to get content provider version from Catima", e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public List<LoyaltyCard> getCards() {
|
||||
final List<LoyaltyCard> cards = new ArrayList<>();
|
||||
final ContentResolver contentResolver = mContext.getContentResolver();
|
||||
try (Cursor cursor = contentResolver.query(cardsUri, null, null, null, null)) {
|
||||
if (cursor == null || cursor.getCount() == 0) {
|
||||
LOG.debug("No cards found");
|
||||
return cards;
|
||||
}
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
final LoyaltyCard loyaltyCard = toLoyaltyCard(cursor);
|
||||
cards.add(loyaltyCard);
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to list cards from Catima", e);
|
||||
return cards;
|
||||
}
|
||||
|
||||
return cards;
|
||||
}
|
||||
|
||||
public List<String> getGroups() {
|
||||
final List<String> groups = new ArrayList<>();
|
||||
final ContentResolver contentResolver = mContext.getContentResolver();
|
||||
try (Cursor cursor = contentResolver.query(groupsUri, null, null, null, null)) {
|
||||
if (cursor == null || cursor.getCount() == 0) {
|
||||
LOG.debug("No groups found");
|
||||
return groups;
|
||||
}
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
final String groupId = cursor.getString(cursor.getColumnIndexOrThrow(LoyaltyCardDbGroups.ID));
|
||||
groups.add(groupId);
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to list groups from Catima", e);
|
||||
return groups;
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the mapping of group to list of card IDs.
|
||||
*
|
||||
* @return the mapping of group to list of card IDS.
|
||||
*/
|
||||
public Map<String, List<Integer>> getGroupCards() {
|
||||
final Map<String, List<Integer>> groupCards = new HashMap<>();
|
||||
final ContentResolver contentResolver = mContext.getContentResolver();
|
||||
try (Cursor cursor = contentResolver.query(cardGroupsUri, null, null, null, null)) {
|
||||
if (cursor == null || cursor.getCount() == 0) {
|
||||
LOG.debug("No card groups found");
|
||||
return groupCards;
|
||||
}
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
final int cardId = cursor.getInt(cursor.getColumnIndexOrThrow(LoyaltyCardDbIdsGroups.cardID));
|
||||
final String groupId = cursor.getString(cursor.getColumnIndexOrThrow(LoyaltyCardDbIdsGroups.groupID));
|
||||
final List<Integer> group;
|
||||
if (groupCards.containsKey(groupId)) {
|
||||
group = groupCards.get(groupId);
|
||||
} else {
|
||||
group = new ArrayList<>();
|
||||
groupCards.put(groupId, group);
|
||||
}
|
||||
group.add(cardId);
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to get group cards from Catima", e);
|
||||
return groupCards;
|
||||
}
|
||||
|
||||
return groupCards;
|
||||
}
|
||||
|
||||
public static LoyaltyCard toLoyaltyCard(final Cursor cursor) {
|
||||
final int id = cursor.getInt(cursor.getColumnIndexOrThrow(LoyaltyCardDbIds.ID));
|
||||
final String name = cursor.getString(cursor.getColumnIndexOrThrow(LoyaltyCardDbIds.STORE));
|
||||
final String note = cursor.getString(cursor.getColumnIndexOrThrow(LoyaltyCardDbIds.NOTE));
|
||||
final long expiryLong = cursor.getLong(cursor.getColumnIndexOrThrow(LoyaltyCardDbIds.EXPIRY));
|
||||
final BigDecimal balance = new BigDecimal(cursor.getString(cursor.getColumnIndexOrThrow(LoyaltyCardDbIds.BALANCE)));
|
||||
final String cardId = cursor.getString(cursor.getColumnIndexOrThrow(LoyaltyCardDbIds.CARD_ID));
|
||||
final String barcodeId = cursor.getString(cursor.getColumnIndexOrThrow(LoyaltyCardDbIds.BARCODE_ID));
|
||||
final int starred = cursor.getInt(cursor.getColumnIndexOrThrow(LoyaltyCardDbIds.STAR_STATUS));
|
||||
final long lastUsed = cursor.getLong(cursor.getColumnIndexOrThrow(LoyaltyCardDbIds.LAST_USED));
|
||||
final int archiveStatus = cursor.getInt(cursor.getColumnIndexOrThrow(LoyaltyCardDbIds.ARCHIVE_STATUS));
|
||||
|
||||
int barcodeTypeColumn = cursor.getColumnIndexOrThrow(LoyaltyCardDbIds.BARCODE_TYPE);
|
||||
int balanceTypeColumn = cursor.getColumnIndexOrThrow(LoyaltyCardDbIds.BALANCE_TYPE);
|
||||
int headerColorColumn = cursor.getColumnIndexOrThrow(LoyaltyCardDbIds.HEADER_COLOR);
|
||||
|
||||
BarcodeFormat barcodeFormat = null;
|
||||
Currency balanceType = null;
|
||||
Date expiry = null;
|
||||
Integer headerColor = null;
|
||||
|
||||
if (!cursor.isNull(barcodeTypeColumn)) {
|
||||
try {
|
||||
barcodeFormat = BarcodeFormat.valueOf(cursor.getString(barcodeTypeColumn));
|
||||
} catch (final IllegalArgumentException e) {
|
||||
LOG.error("Unknown barcode format {}", barcodeTypeColumn);
|
||||
}
|
||||
}
|
||||
|
||||
if (!cursor.isNull(balanceTypeColumn)) {
|
||||
balanceType = Currency.getInstance(cursor.getString(balanceTypeColumn));
|
||||
}
|
||||
|
||||
if (expiryLong > 0) {
|
||||
expiry = new Date(expiryLong);
|
||||
}
|
||||
|
||||
if (!cursor.isNull(headerColorColumn)) {
|
||||
headerColor = cursor.getInt(headerColorColumn);
|
||||
}
|
||||
|
||||
return new LoyaltyCard(
|
||||
id,
|
||||
name,
|
||||
note,
|
||||
expiry,
|
||||
balance,
|
||||
balanceType,
|
||||
cardId,
|
||||
barcodeId,
|
||||
barcodeFormat,
|
||||
headerColor,
|
||||
starred != 0,
|
||||
archiveStatus != 0,
|
||||
lastUsed
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copied from Catima, protect.card_locker.DBHelper.LoyaltyCardDbGroups.
|
||||
* Commit: 8607e1c2
|
||||
*/
|
||||
public static class LoyaltyCardDbGroups {
|
||||
public static final String TABLE = "groups";
|
||||
public static final String ID = "_id";
|
||||
public static final String ORDER = "orderId";
|
||||
}
|
||||
|
||||
/**
|
||||
* Copied from Catima, protect.card_locker.DBHelper.LoyaltyCardDbIds.
|
||||
* Commit: 8607e1c2
|
||||
*/
|
||||
public static class LoyaltyCardDbIds {
|
||||
public static final String TABLE = "cards";
|
||||
public static final String ID = "_id";
|
||||
public static final String STORE = "store";
|
||||
public static final String EXPIRY = "expiry";
|
||||
public static final String BALANCE = "balance";
|
||||
public static final String BALANCE_TYPE = "balancetype";
|
||||
public static final String NOTE = "note";
|
||||
public static final String HEADER_COLOR = "headercolor";
|
||||
public static final String HEADER_TEXT_COLOR = "headertextcolor";
|
||||
public static final String CARD_ID = "cardid";
|
||||
public static final String BARCODE_ID = "barcodeid";
|
||||
public static final String BARCODE_TYPE = "barcodetype";
|
||||
public static final String STAR_STATUS = "starstatus";
|
||||
public static final String LAST_USED = "lastused";
|
||||
public static final String ZOOM_LEVEL = "zoomlevel";
|
||||
public static final String ARCHIVE_STATUS = "archive";
|
||||
}
|
||||
|
||||
/**
|
||||
* Copied from Catima, protect.card_locker.DBHelper.LoyaltyCardDbIdsGroups.
|
||||
* Commit: 8607e1c2
|
||||
*/
|
||||
public static class LoyaltyCardDbIdsGroups {
|
||||
public static final String TABLE = "cardsGroups";
|
||||
public static final String cardID = "cardId";
|
||||
public static final String groupID = "groupId";
|
||||
}
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
/* Copyright (C) 2023 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.capabilities.loyaltycards;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsConst.LOYALTY_CARDS_CATIMA_PACKAGE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsConst.LOYALTY_CARDS_SYNC_ARCHIVED;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsConst.LOYALTY_CARDS_SYNC_GROUPS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsConst.LOYALTY_CARDS_SYNC_GROUPS_ONLY;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsConst.LOYALTY_CARDS_SYNC_STARRED;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public class CatimaManager {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CatimaManager.class);
|
||||
|
||||
private final Context context;
|
||||
|
||||
public CatimaManager(final Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public void sync(final GBDevice gbDevice) {
|
||||
final List<CharSequence> installedCatimaPackages = findInstalledCatimaPackages();
|
||||
if (installedCatimaPackages.isEmpty()) {
|
||||
LOG.warn("Catima is not installed");
|
||||
return;
|
||||
}
|
||||
|
||||
final Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()));
|
||||
|
||||
final boolean syncGroupsOnly = prefs.getBoolean(LOYALTY_CARDS_SYNC_GROUPS_ONLY, false);
|
||||
final Set<String> syncGroups = prefs.getStringSet(LOYALTY_CARDS_SYNC_GROUPS, Collections.emptySet());
|
||||
final boolean syncArchived = prefs.getBoolean(LOYALTY_CARDS_SYNC_ARCHIVED, false);
|
||||
final boolean syncStarred = prefs.getBoolean(LOYALTY_CARDS_SYNC_STARRED, false);
|
||||
|
||||
final String catimaPackage = prefs.getString(LOYALTY_CARDS_CATIMA_PACKAGE, installedCatimaPackages.get(0).toString());
|
||||
final CatimaContentProvider catima = new CatimaContentProvider(context, catimaPackage);
|
||||
|
||||
if (!catima.isCatimaCompatible()) {
|
||||
LOG.warn("Catima is not compatible");
|
||||
return;
|
||||
}
|
||||
|
||||
final List<LoyaltyCard> cards = catima.getCards();
|
||||
final Map<String, List<Integer>> groupCards = catima.getGroupCards();
|
||||
|
||||
final Set<Integer> cardsInGroupsToSync = new HashSet<>();
|
||||
if (syncGroupsOnly) {
|
||||
for (final Map.Entry<String, List<Integer>> groupCardsEntry : groupCards.entrySet()) {
|
||||
if (syncGroups.contains(groupCardsEntry.getKey())) {
|
||||
cardsInGroupsToSync.addAll(groupCardsEntry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final ArrayList<LoyaltyCard> cardsToSync = new ArrayList<>();
|
||||
for (final LoyaltyCard card : cards) {
|
||||
if (syncGroupsOnly && !cardsInGroupsToSync.contains(card.getId())) {
|
||||
continue;
|
||||
}
|
||||
if (!syncArchived && card.isArchived()) {
|
||||
continue;
|
||||
}
|
||||
if (syncStarred && !card.isStarred()) {
|
||||
continue;
|
||||
}
|
||||
cardsToSync.add(card);
|
||||
}
|
||||
|
||||
Collections.sort(cardsToSync);
|
||||
|
||||
LOG.debug("Will sync cards: {}", cardsToSync);
|
||||
|
||||
GB.toast(context, context.getString(R.string.loyalty_cards_syncing, cardsToSync.size()), Toast.LENGTH_LONG, GB.INFO);
|
||||
|
||||
GBApplication.deviceService(gbDevice).onSetLoyaltyCards(cardsToSync);
|
||||
}
|
||||
|
||||
public List<CharSequence> findInstalledCatimaPackages() {
|
||||
final List<CharSequence> installedCatimaPackages = new ArrayList<>();
|
||||
for (final String knownPackage : CatimaContentProvider.KNOWN_PACKAGES) {
|
||||
if (isPackageInstalled(knownPackage)) {
|
||||
installedCatimaPackages.add(knownPackage);
|
||||
}
|
||||
}
|
||||
return installedCatimaPackages;
|
||||
}
|
||||
|
||||
private boolean isPackageInstalled(final String packageName) {
|
||||
try {
|
||||
return context.getPackageManager().getApplicationInfo(packageName, 0).enabled;
|
||||
} catch (final PackageManager.NameNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,153 @@
|
||||
/* Copyright (C) 2023 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.capabilities.loyaltycards;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.apache.commons.lang3.builder.CompareToBuilder;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Currency;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
public class LoyaltyCard implements Serializable, Comparable<LoyaltyCard> {
|
||||
|
||||
private final int id;
|
||||
private final String name;
|
||||
private final String note;
|
||||
private final Date expiry;
|
||||
private final BigDecimal balance;
|
||||
private final Currency balanceType;
|
||||
private final String cardId;
|
||||
|
||||
@Nullable
|
||||
private final String barcodeId;
|
||||
|
||||
@Nullable
|
||||
private final BarcodeFormat barcodeFormat;
|
||||
|
||||
@Nullable
|
||||
private final Integer color;
|
||||
|
||||
private final boolean starred;
|
||||
private final boolean archived;
|
||||
private final long lastUsed;
|
||||
|
||||
public LoyaltyCard(final int id,
|
||||
final String name,
|
||||
final String note,
|
||||
final Date expiry,
|
||||
final BigDecimal balance,
|
||||
final Currency balanceType,
|
||||
final String cardId,
|
||||
@Nullable final String barcodeId,
|
||||
@Nullable final BarcodeFormat barcodeFormat,
|
||||
@Nullable final Integer color,
|
||||
final boolean starred,
|
||||
final boolean archived,
|
||||
final long lastUsed) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.note = note;
|
||||
this.expiry = expiry;
|
||||
this.balance = balance;
|
||||
this.balanceType = balanceType;
|
||||
this.cardId = cardId;
|
||||
this.barcodeId = barcodeId;
|
||||
this.barcodeFormat = barcodeFormat;
|
||||
this.color = color;
|
||||
this.starred = starred;
|
||||
this.archived = archived;
|
||||
this.lastUsed = lastUsed;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getNote() {
|
||||
return note;
|
||||
}
|
||||
|
||||
public Date getExpiry() {
|
||||
return expiry;
|
||||
}
|
||||
|
||||
public BigDecimal getBalance() {
|
||||
return balance;
|
||||
}
|
||||
|
||||
public Currency getBalanceType() {
|
||||
return balanceType;
|
||||
}
|
||||
|
||||
public String getCardId() {
|
||||
return cardId;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getBarcodeId() {
|
||||
return barcodeId;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public BarcodeFormat getBarcodeFormat() {
|
||||
return barcodeFormat;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Integer getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
public boolean isStarred() {
|
||||
return starred;
|
||||
}
|
||||
|
||||
public boolean isArchived() {
|
||||
return archived;
|
||||
}
|
||||
|
||||
public long getLastUsed() {
|
||||
return lastUsed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format(
|
||||
Locale.ROOT,
|
||||
"LoyaltyCard{id=%s, name=%s, cardId=%s}",
|
||||
id, name, cardId
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(final LoyaltyCard o) {
|
||||
return new CompareToBuilder()
|
||||
.append(isStarred(), o.isStarred())
|
||||
.append(isArchived(), o.isArchived())
|
||||
.append(getName(), o.getName())
|
||||
.append(getCardId(), o.getCardId())
|
||||
.build();
|
||||
}
|
||||
}
|
@ -24,6 +24,7 @@ import android.net.Uri;
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.loyaltycards.LoyaltyCard;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
@ -53,6 +54,8 @@ public interface EventHandler {
|
||||
|
||||
void onSetReminders(ArrayList<? extends Reminder> reminders);
|
||||
|
||||
void onSetLoyaltyCards(ArrayList<LoyaltyCard> cards);
|
||||
|
||||
void onSetWorldClocks(ArrayList<? extends WorldClock> clocks);
|
||||
|
||||
void onSetContacts(ArrayList<? extends Contact> contacts);
|
||||
|
@ -52,6 +52,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuami2
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsAlexaService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsContactsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsLogsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsLoyaltyCardService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsRemindersService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsShortcutCardsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsConfigService;
|
||||
@ -277,6 +278,15 @@ public abstract class Huami2021Coordinator extends HuamiCoordinator {
|
||||
public int[] getSupportedDeviceSpecificSettings(final GBDevice device) {
|
||||
final List<Integer> settings = new ArrayList<>();
|
||||
|
||||
//
|
||||
// Apps
|
||||
// TODO: These should go somewhere else
|
||||
//
|
||||
settings.add(R.xml.devicesettings_header_apps);
|
||||
if (ZeppOsLoyaltyCardService.isSupported(getPrefs(device))) {
|
||||
settings.add(R.xml.devicesettings_loyalty_cards);
|
||||
}
|
||||
|
||||
//
|
||||
// Time
|
||||
//
|
||||
|
@ -42,6 +42,7 @@ import java.util.Set;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.GpsCapability;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiVibrationPatternNotificationType;
|
||||
@ -199,6 +200,9 @@ public class Huami2021SettingsCustomizer extends HuamiSettingsCustomizer {
|
||||
}
|
||||
|
||||
// Hides the headers if none of the preferences under them are available
|
||||
hidePrefIfNoneVisible(handler, DeviceSettingsPreferenceConst.PREF_HEADER_APPS, Arrays.asList(
|
||||
LoyaltyCardsSettingsConst.PREF_KEY_LOYALTY_CARDS
|
||||
));
|
||||
hidePrefIfNoneVisible(handler, DeviceSettingsPreferenceConst.PREF_HEADER_TIME, Arrays.asList(
|
||||
DeviceSettingsPreferenceConst.PREF_TIMEFORMAT,
|
||||
DeviceSettingsPreferenceConst.PREF_DATEFORMAT,
|
||||
|
@ -25,16 +25,15 @@ import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.location.Location;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Parcelable;
|
||||
import android.provider.ContactsContract;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.loyaltycards.LoyaltyCard;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
@ -263,6 +262,13 @@ public class GBDeviceService implements DeviceService {
|
||||
invokeService(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetLoyaltyCards(final ArrayList<LoyaltyCard> cards) {
|
||||
final Intent intent = createIntent().setAction(ACTION_SET_LOYALTY_CARDS)
|
||||
.putExtra(EXTRA_LOYALTY_CARDS, cards);
|
||||
invokeService(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetWorldClocks(ArrayList<? extends WorldClock> clocks) {
|
||||
Intent intent = createIntent().setAction(ACTION_SET_WORLD_CLOCKS)
|
||||
|
@ -60,6 +60,7 @@ public interface DeviceService extends EventHandler {
|
||||
String ACTION_SET_ALARMS = PREFIX + ".action.set_alarms";
|
||||
String ACTION_SAVE_ALARMS = PREFIX + ".action.save_alarms";
|
||||
String ACTION_SET_REMINDERS = PREFIX + ".action.set_reminders";
|
||||
String ACTION_SET_LOYALTY_CARDS = PREFIX + ".action.set_loyalty_cards";
|
||||
String ACTION_SET_WORLD_CLOCKS = PREFIX + ".action.set_world_clocks";
|
||||
String ACTION_SET_CONTACTS = PREFIX + ".action.set_contacts";
|
||||
String ACTION_ENABLE_REALTIME_STEPS = PREFIX + ".action.enable_realtime_steps";
|
||||
@ -123,6 +124,7 @@ public interface DeviceService extends EventHandler {
|
||||
String EXTRA_CONFIG = "config";
|
||||
String EXTRA_ALARMS = "alarms";
|
||||
String EXTRA_REMINDERS = "reminders";
|
||||
String EXTRA_LOYALTY_CARDS = "loyalty_cards";
|
||||
String EXTRA_WORLD_CLOCKS = "world_clocks";
|
||||
String EXTRA_CONTACTS = "contacts";
|
||||
String EXTRA_CONNECT_FIRST_TIME = "connect_first_time";
|
||||
|
@ -56,6 +56,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.FindPhoneActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AbstractAppManagerFragment;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.loyaltycards.LoyaltyCard;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBAccess;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
@ -637,6 +638,16 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* If loyalty cards can be set on the device, this method can be
|
||||
* overridden and implemented by the device support class.
|
||||
* @param cards {@link java.util.ArrayList} containing {@link LoyaltyCard} instances
|
||||
*/
|
||||
@Override
|
||||
public void onSetLoyaltyCards(ArrayList<LoyaltyCard> cards) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* If world clocks can be configured on the device, this method can be
|
||||
* overridden and implemented by the device support class.
|
||||
|
@ -53,6 +53,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.loyaltycards.LoyaltyCard;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.AlarmClockReceiver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.AlarmReceiver;
|
||||
@ -135,6 +136,7 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SE
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_HEARTRATE_MEASUREMENT_INTERVAL;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_GPS_LOCATION;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_LED_COLOR;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_LOYALTY_CARDS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_PHONE_VOLUME;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_REMINDERS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_WORLD_CLOCKS;
|
||||
@ -172,6 +174,7 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_FM_
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_GPS_LOCATION;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_INTERVAL_SECONDS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_LED_COLOR;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_LOYALTY_CARDS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_ALBUM;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_ARTIST;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_DURATION;
|
||||
@ -946,6 +949,10 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
|
||||
ArrayList<? extends Reminder> reminders = (ArrayList<? extends Reminder>) intent.getSerializableExtra(EXTRA_REMINDERS);
|
||||
deviceSupport.onSetReminders(reminders);
|
||||
break;
|
||||
case ACTION_SET_LOYALTY_CARDS:
|
||||
final ArrayList<LoyaltyCard> loyaltyCards = (ArrayList<LoyaltyCard>) intent.getSerializableExtra(EXTRA_LOYALTY_CARDS);
|
||||
deviceSupport.onSetLoyaltyCards(loyaltyCards);
|
||||
break;
|
||||
case ACTION_SET_WORLD_CLOCKS:
|
||||
ArrayList<? extends WorldClock> clocks = (ArrayList<? extends WorldClock>) intent.getSerializableExtra(EXTRA_WORLD_CLOCKS);
|
||||
deviceSupport.onSetWorldClocks(clocks);
|
||||
|
@ -31,6 +31,7 @@ import java.util.Arrays;
|
||||
import java.util.EnumSet;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.loyaltycards.LoyaltyCard;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
|
||||
@ -373,6 +374,13 @@ public class ServiceDeviceSupport implements DeviceSupport {
|
||||
delegate.onSetContacts(contacts);
|
||||
}
|
||||
|
||||
public void onSetLoyaltyCards(final ArrayList<LoyaltyCard> cards) {
|
||||
if (checkBusy("set loyalty cards")) {
|
||||
return;
|
||||
}
|
||||
delegate.onSetLoyaltyCards(cards);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetWorldClocks(ArrayList<? extends WorldClock> clocks) {
|
||||
if (checkBusy("set world clocks")) {
|
||||
|
@ -75,6 +75,7 @@ import java.util.regex.Pattern;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.loyaltycards.LoyaltyCard;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventScreenshot;
|
||||
@ -121,6 +122,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.service
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsDisplayItemsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsHttpService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsLogsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsLoyaltyCardService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsNotificationService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsRemindersService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsServicesService;
|
||||
@ -170,6 +172,7 @@ public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFil
|
||||
private final ZeppOsDisplayItemsService displayItemsService = new ZeppOsDisplayItemsService(this);
|
||||
private final ZeppOsHttpService httpService = new ZeppOsHttpService(this);
|
||||
private final ZeppOsRemindersService remindersService = new ZeppOsRemindersService(this);
|
||||
private final ZeppOsLoyaltyCardService loyaltyCardService = new ZeppOsLoyaltyCardService(this);
|
||||
|
||||
private final Map<Short, AbstractZeppOsService> mServiceMap = new LinkedHashMap<Short, AbstractZeppOsService>() {{
|
||||
put(servicesService.getEndpoint(), servicesService);
|
||||
@ -193,6 +196,7 @@ public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFil
|
||||
put(displayItemsService.getEndpoint(), displayItemsService);
|
||||
put(httpService.getEndpoint(), httpService);
|
||||
put(remindersService.getEndpoint(), remindersService);
|
||||
put(loyaltyCardService.getEndpoint(), loyaltyCardService);
|
||||
}};
|
||||
|
||||
public Huami2021Support() {
|
||||
@ -514,6 +518,11 @@ public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFil
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetLoyaltyCards(final ArrayList<LoyaltyCard> cards) {
|
||||
loyaltyCardService.setCards(cards);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetContacts(ArrayList<? extends Contact> contacts) {
|
||||
contactsService.setContacts((List<Contact>) contacts);
|
||||
|
@ -0,0 +1,266 @@
|
||||
/* Copyright (C) 2023 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.loyaltycards.BarcodeFormat;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.loyaltycards.LoyaltyCard;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021Support;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.MapUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public class ZeppOsLoyaltyCardService extends AbstractZeppOsService {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsLoyaltyCardService.class);
|
||||
|
||||
private static final short ENDPOINT = 0x003c;
|
||||
|
||||
private static final byte CMD_CAPABILITIES_REQUEST = 0x01;
|
||||
private static final byte CMD_CAPABILITIES_RESPONSE = 0x02;
|
||||
private static final byte CMD_REQUEST = 0x05;
|
||||
private static final byte CMD_RESPONSE = 0x06;
|
||||
private static final byte CMD_SET = 0x03;
|
||||
private static final byte CMD_SET_ACK = 0x04;
|
||||
private static final byte CMD_UPDATE = 0x07;
|
||||
private static final byte CMD_UPDATE_ACK = 0x08;
|
||||
private static final byte CMD_ADD = 0x09;
|
||||
private static final byte CMD_ADD_ACK = 0x0a;
|
||||
|
||||
private final List<BarcodeFormat> supportedFormats = new ArrayList<>();
|
||||
private final List<Integer> supportedColors = new ArrayList<>();
|
||||
|
||||
public static final String PREF_VERSION = "zepp_os_loyalty_cards_version";
|
||||
|
||||
public ZeppOsLoyaltyCardService(final Huami2021Support support) {
|
||||
super(support);
|
||||
}
|
||||
|
||||
@Override
|
||||
public short getEndpoint() {
|
||||
return ENDPOINT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEncrypted() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePayload(final byte[] payload) {
|
||||
switch (payload[0]) {
|
||||
case CMD_CAPABILITIES_RESPONSE:
|
||||
LOG.info("Loyalty cards capabilities, version1={}, version2={}", payload[1], payload[2]);
|
||||
|
||||
supportedFormats.clear();
|
||||
supportedColors.clear();
|
||||
int version = payload[1];
|
||||
|
||||
if (version != 1 || payload[2] != 1) {
|
||||
LOG.warn("Unexpected loyalty cards service version");
|
||||
return;
|
||||
}
|
||||
|
||||
int pos = 3;
|
||||
|
||||
final byte numSupportedCardTypes = payload[pos++];
|
||||
final Map<Byte, BarcodeFormat> barcodeFormatCodes = MapUtils.reverse(BARCODE_FORMAT_CODES);
|
||||
for (int i = 0; i < numSupportedCardTypes; i++, pos++) {
|
||||
final BarcodeFormat barcodeFormat = barcodeFormatCodes.get(payload[pos]);
|
||||
if (barcodeFormat == null) {
|
||||
LOG.warn("Unknown barcode format {}", String.format("0x%02x", payload[pos]));
|
||||
continue;
|
||||
}
|
||||
supportedFormats.add(barcodeFormat);
|
||||
}
|
||||
|
||||
final byte numSupportedColors = payload[pos++];
|
||||
final Map<Byte, Integer> colorCodes = MapUtils.reverse(COLOR_CODES);
|
||||
for (int i = 0; i < numSupportedColors; i++) {
|
||||
final Integer color = colorCodes.get(payload[pos]);
|
||||
if (color == null) {
|
||||
LOG.warn("Unknown color {}", String.format("0x%02x", payload[pos]));
|
||||
continue;
|
||||
}
|
||||
supportedColors.add(color);
|
||||
}
|
||||
|
||||
getSupport().evaluateGBDeviceEvent(new GBDeviceEventUpdatePreferences(PREF_VERSION, version));
|
||||
return;
|
||||
case CMD_SET_ACK:
|
||||
LOG.info("Loyalty cards set ACK, status = {}", payload[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.warn("Unexpected loyalty cards byte {}", String.format("0x%02x", payload[0]));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(final TransactionBuilder builder) {
|
||||
requestCapabilities(builder);
|
||||
}
|
||||
|
||||
public boolean isSupported() {
|
||||
return !supportedFormats.isEmpty() && !supportedColors.isEmpty();
|
||||
}
|
||||
|
||||
public List<BarcodeFormat> getSupportedFormats() {
|
||||
return supportedFormats;
|
||||
}
|
||||
|
||||
public void requestCapabilities(final TransactionBuilder builder) {
|
||||
write(builder, CMD_CAPABILITIES_REQUEST);
|
||||
}
|
||||
|
||||
public void setCards(final List<LoyaltyCard> cards) {
|
||||
LOG.info("Setting {} loyalty cards", cards.size());
|
||||
|
||||
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
||||
final List<LoyaltyCard> supportedCards = filterSupportedCards(cards);
|
||||
|
||||
baos.write(CMD_SET);
|
||||
baos.write(supportedCards.size());
|
||||
|
||||
for (final LoyaltyCard card : supportedCards) {
|
||||
try {
|
||||
baos.write(encodeCard(card));
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to encode card", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
write("set loyalty cards", baos.toByteArray());
|
||||
}
|
||||
|
||||
private List<LoyaltyCard> filterSupportedCards(final List<LoyaltyCard> cards) {
|
||||
final List<LoyaltyCard> ret = new ArrayList<>();
|
||||
|
||||
for (final LoyaltyCard card : cards) {
|
||||
if (supportedFormats.contains(card.getBarcodeFormat())) {
|
||||
ret.add(card);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private byte[] encodeCard(final LoyaltyCard card) {
|
||||
final Byte barcodeFormatCode = BARCODE_FORMAT_CODES.get(card.getBarcodeFormat());
|
||||
if (barcodeFormatCode == null) {
|
||||
LOG.error("Unsupported barcode format {}", card.getBarcodeFormat());
|
||||
return null;
|
||||
}
|
||||
|
||||
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
baos.write(card.getName().getBytes(StandardCharsets.UTF_8));
|
||||
baos.write(0);
|
||||
|
||||
// This is optional
|
||||
baos.write(card.getCardId().getBytes(StandardCharsets.UTF_8));
|
||||
baos.write(0);
|
||||
|
||||
if (card.getBarcodeId() != null) {
|
||||
baos.write(card.getBarcodeId().getBytes(StandardCharsets.UTF_8));
|
||||
} else {
|
||||
baos.write(card.getCardId().getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
baos.write(0);
|
||||
|
||||
baos.write(barcodeFormatCode);
|
||||
if (card.getColor() != null) {
|
||||
baos.write(findNearestColorCode(card.getColor()));
|
||||
} else {
|
||||
baos.write(0x00);
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to encode card", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
private byte findNearestColorCode(final int color) {
|
||||
final double r = ((color >> 16) & 0xff) / 255f;
|
||||
final double g = ((color >> 8) & 0xff) / 255f;
|
||||
final double b = (color & 0xff) / 255f;
|
||||
|
||||
int nearestColor = 0x66c6ea;
|
||||
double minDistance = Float.MAX_VALUE;
|
||||
|
||||
// TODO better color distance algorithm?
|
||||
for (final Integer colorPreset : COLOR_CODES.keySet()) {
|
||||
final double rPreset = ((colorPreset >> 16) & 0xff) / 255f;
|
||||
final double gPreset = ((colorPreset >> 8) & 0xff) / 255f;
|
||||
final double bPreset = (colorPreset & 0xff) / 255f;
|
||||
|
||||
final double distance = Math.sqrt(Math.pow(rPreset - r, 2) + Math.pow(gPreset - g, 2) + Math.pow(bPreset - b, 2));
|
||||
if (distance < minDistance) {
|
||||
nearestColor = colorPreset;
|
||||
minDistance = distance;
|
||||
}
|
||||
}
|
||||
|
||||
return Objects.requireNonNull(COLOR_CODES.get(nearestColor));
|
||||
}
|
||||
|
||||
private static final Map<BarcodeFormat, Byte> BARCODE_FORMAT_CODES = new HashMap<BarcodeFormat, Byte>() {{
|
||||
put(BarcodeFormat.CODE_128, (byte) 0x00);
|
||||
put(BarcodeFormat.CODE_39, (byte) 0x01);
|
||||
put(BarcodeFormat.QR_CODE, (byte) 0x03);
|
||||
put(BarcodeFormat.UPC_A, (byte) 0x06);
|
||||
put(BarcodeFormat.EAN_13, (byte) 0x07);
|
||||
put(BarcodeFormat.EAN_8, (byte) 0x08);
|
||||
}};
|
||||
|
||||
/**
|
||||
* Map or RGB color to color byte - the watches only support color presets.
|
||||
*/
|
||||
private static final Map<Integer, Byte> COLOR_CODES = new HashMap<Integer, Byte>() {{
|
||||
put(0x66c6ea, (byte) 0x00); // Light blue
|
||||
put(0x008fc5, (byte) 0x01); // Blue
|
||||
put(0xc19ffd, (byte) 0x02); // Light purple
|
||||
put(0x8855e2, (byte) 0x03); // Purple
|
||||
put(0xfb8e89, (byte) 0x04); // Light red
|
||||
put(0xdf3b34, (byte) 0x05); // Red
|
||||
put(0xffab03, (byte) 0x06); // Orange
|
||||
put(0xffaa77, (byte) 0x07); // Light Orange
|
||||
put(0xe75800, (byte) 0x08); // Dark Orange
|
||||
put(0x66d0b8, (byte) 0x09); // Light green
|
||||
put(0x009e7a, (byte) 0x0a); // Green
|
||||
put(0xffcd68, (byte) 0x0b); // Yellow-ish
|
||||
}};
|
||||
|
||||
public static boolean isSupported(final Prefs devicePrefs) {
|
||||
return devicePrefs.getInt(PREF_VERSION, 0) == 1;
|
||||
}
|
||||
}
|
10
app/src/main/res/drawable/ic_archive.xml
Normal file
10
app/src/main/res/drawable/ic_archive.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#7E7E7E"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M20.54,5.23l-1.39,-1.68C18.88,3.21 18.47,3 18,3H6c-0.47,0 -0.88,0.21 -1.16,0.55L3.46,5.23C3.17,5.57 3,6.02 3,6.5V19c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V6.5c0,-0.48 -0.17,-0.93 -0.46,-1.27zM12,17.5L6.5,12H10v-2h4v2h3.5L12,17.5zM5.12,5l0.81,-1h12l0.94,1H5.12z" />
|
||||
</vector>
|
10
app/src/main/res/drawable/ic_loyalty.xml
Normal file
10
app/src/main/res/drawable/ic_loyalty.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#7E7E7E"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M21.41,11.58l-9,-9C12.05,2.22 11.55,2 11,2L4,2c-1.1,0 -2,0.9 -2,2v7c0,0.55 0.22,1.05 0.59,1.42l9,9c0.36,0.36 0.86,0.58 1.41,0.58 0.55,0 1.05,-0.22 1.41,-0.59l7,-7c0.37,-0.36 0.59,-0.86 0.59,-1.41 0,-0.55 -0.23,-1.06 -0.59,-1.42zM5.5,7C4.67,7 4,6.33 4,5.5S4.67,4 5.5,4 7,4.67 7,5.5 6.33,7 5.5,7zM17.27,15.27L13,19.54l-4.27,-4.27C8.28,14.81 8,14.19 8,13.5c0,-1.38 1.12,-2.5 2.5,-2.5 0.69,0 1.32,0.28 1.77,0.74l0.73,0.72 0.73,-0.73c0.45,-0.45 1.08,-0.73 1.77,-0.73 1.38,0 2.5,1.12 2.5,2.5 0,0.69 -0.28,1.32 -0.73,1.77z" />
|
||||
</vector>
|
12
app/src/main/res/layout/activity_loyalty_cards.xml
Normal file
12
app/src/main/res/layout/activity_loyalty_cards.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="10dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/settings_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</LinearLayout>
|
@ -2155,4 +2155,23 @@
|
||||
<string name="changelog_show_full">More…</string>
|
||||
<string name="changelog_ok_button">OK</string>
|
||||
<string name="changelog_title">What\'s New</string>
|
||||
<string name="loyalty_cards_catima_package">Catima package name</string>
|
||||
<string name="loyalty_cards_install">Install Catima</string>
|
||||
<string name="loyalty_cards_sync_groups_only">Sync only specific groups</string>
|
||||
<string name="loyalty_cards_sync_groups">Groups to Sync</string>
|
||||
<string name="loyalty_cards_sync_archived">Sync archived cards</string>
|
||||
<string name="loyalty_cards_install_catima_fail">Failed to open app store to install Catima</string>
|
||||
<string name="loyalty_cards_sync_starred">Sync only starred cards</string>
|
||||
<string name="loyalty_cards_sync_title">Sync Loyalty Cards</string>
|
||||
<string name="loyalty_cards_sync_summary">Tap to sync the cards to the watch</string>
|
||||
<string name="loyalty_cards_catima_not_installed">Catima is needed to manage the loyalty cards</string>
|
||||
<string name="loyalty_cards_catima_not_compatible">The installed Catima version is not compatible with Gadgetbridge. Please update Catima and Gadgetbridge to the latest versions.</string>
|
||||
<string name="loyalty_cards_sync_options">Sync Options</string>
|
||||
<string name="loyalty_cards_sync">Sync</string>
|
||||
<string name="loyalty_cards_catima">Catima</string>
|
||||
<string name="loyalty_cards_open_catima">Open Catima</string>
|
||||
<string name="loyalty_cards_catima_permissions_title">Missing permissions</string>
|
||||
<string name="loyalty_cards_catima_permissions_summary">Gadgetbridge needs read permissions on Catima cards to sync them. Tap this button to grant them.</string>
|
||||
<string name="loyalty_cards">Loyalty Cards</string>
|
||||
<string name="loyalty_cards_syncing">Syncing %d loyalty cards to device</string>
|
||||
</resources>
|
||||
|
6
app/src/main/res/xml/devicesettings_header_apps.xml
Normal file
6
app/src/main/res/xml/devicesettings_header_apps.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<PreferenceCategory
|
||||
android:key="pref_header_apps"
|
||||
android:title="@string/qhybrid_title_apps" />
|
||||
</androidx.preference.PreferenceScreen>
|
7
app/src/main/res/xml/devicesettings_loyalty_cards.xml
Normal file
7
app/src/main/res/xml/devicesettings_loyalty_cards.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<Preference
|
||||
android:icon="@drawable/ic_loyalty"
|
||||
android:key="pref_key_loyalty_cards"
|
||||
android:title="@string/loyalty_cards" />
|
||||
</androidx.preference.PreferenceScreen>
|
86
app/src/main/res/xml/loyalty_cards.xml
Normal file
86
app/src/main/res/xml/loyalty_cards.xml
Normal file
@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<PreferenceCategory
|
||||
android:key="pref_key_header_loyalty_cards_catima"
|
||||
android:title="@string/loyalty_cards_catima">
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="me.hackerchick.catima"
|
||||
android:entries="@array/pref_huami2021_empty_array"
|
||||
android:entryValues="@array/pref_huami2021_empty_array"
|
||||
android:key="loyalty_cards_catima_package"
|
||||
android:summary="%s"
|
||||
android:title="@string/loyalty_cards_catima_package" />
|
||||
|
||||
<Preference
|
||||
android:icon="@drawable/ic_loyalty"
|
||||
android:key="loyalty_cards_open_catima"
|
||||
android:title="@string/loyalty_cards_open_catima" />
|
||||
|
||||
<Preference
|
||||
android:icon="@drawable/ic_warning_gray"
|
||||
android:key="loyalty_cards_catima_not_installed"
|
||||
android:summary="@string/loyalty_cards_catima_not_installed" />
|
||||
|
||||
<Preference
|
||||
android:icon="@drawable/ic_warning_gray"
|
||||
android:key="loyalty_cards_catima_not_compatible"
|
||||
android:summary="@string/loyalty_cards_catima_not_compatible" />
|
||||
|
||||
<Preference
|
||||
android:icon="@drawable/ic_loyalty"
|
||||
android:key="loyalty_cards_install_catima"
|
||||
android:title="@string/loyalty_cards_install" />
|
||||
|
||||
<Preference
|
||||
android:icon="@drawable/ic_warning_gray"
|
||||
android:key="loyalty_cards_catima_permissions"
|
||||
android:summary="@string/loyalty_cards_catima_permissions_summary"
|
||||
android:title="@string/loyalty_cards_catima_permissions_title" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:key="pref_key_header_loyalty_cards_sync"
|
||||
android:title="@string/loyalty_cards_sync">
|
||||
|
||||
<Preference
|
||||
android:icon="@drawable/ic_refresh"
|
||||
android:key="loyalty_cards_sync"
|
||||
android:summary="@string/loyalty_cards_sync_summary"
|
||||
android:title="@string/loyalty_cards_sync_title" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:key="pref_key_header_loyalty_cards_sync_options"
|
||||
android:title="@string/loyalty_cards_sync_options">
|
||||
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:icon="@drawable/ic_list_numbered"
|
||||
android:key="loyalty_cards_sync_groups_only"
|
||||
android:title="@string/loyalty_cards_sync_groups_only" />
|
||||
|
||||
<MultiSelectListPreference
|
||||
android:defaultValue="@array/pref_huami2021_empty_array"
|
||||
android:dependency="loyalty_cards_sync_groups_only"
|
||||
android:dialogTitle="@string/loyalty_cards_sync_groups"
|
||||
android:entries="@array/pref_huami2021_empty_array"
|
||||
android:entryValues="@array/pref_huami2021_empty_array"
|
||||
android:icon="@drawable/ic_checklist"
|
||||
android:key="loyalty_cards_sync_groups"
|
||||
android:summary=""
|
||||
android:title="@string/loyalty_cards_sync_groups" />
|
||||
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:icon="@drawable/ic_archive"
|
||||
android:key="loyalty_cards_sync_archived"
|
||||
android:title="@string/loyalty_cards_sync_archived" />
|
||||
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:icon="@drawable/ic_star_gray"
|
||||
android:key="loyalty_cards_sync_starred"
|
||||
android:title="@string/loyalty_cards_sync_starred" />
|
||||
</PreferenceCategory>
|
||||
</androidx.preference.PreferenceScreen>
|
Loading…
Reference in New Issue
Block a user