mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Huawei: Ability to upload notification app icons
This commit is contained in:
@@ -225,6 +225,10 @@
|
||||
android:name=".devices.huawei.ui.HuaweiStressCalibrationActivity"
|
||||
android:label="@string/huawei_stress_calibrate"
|
||||
android:parentActivityName=".activities.devicesettings.DeviceSettingsActivity" />
|
||||
<activity
|
||||
android:name=".activities.NotificationsAppIconUploadActivity"
|
||||
android:label="@string/notifications_app_icon_upload"
|
||||
android:parentActivityName=".activities.devicesettings.DeviceSettingsActivity" />
|
||||
<activity
|
||||
android:name=".activities.ActivitySummariesActivity"
|
||||
android:label="@string/activity_summaries"
|
||||
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
/* Copyright (C) 2025 Me7c7
|
||||
|
||||
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 <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.adapter.NotificationsAppIconAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.NotificationUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public class NotificationsAppIconUploadActivity extends AbstractGBActivity {
|
||||
|
||||
protected GBDevice mGBDevice = null;
|
||||
|
||||
private NotificationsAppIconAdapter appsAdapter = null;
|
||||
|
||||
private RecyclerView appsListView;
|
||||
|
||||
private View loadingView;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.activity_notifications_app_icon_upload);
|
||||
|
||||
Bundle extras = getIntent().getExtras();
|
||||
if (extras != null) {
|
||||
mGBDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE);
|
||||
}
|
||||
if (mGBDevice == null) {
|
||||
throw new IllegalArgumentException("Must provide a device when invoking this activity");
|
||||
}
|
||||
|
||||
Button uploadToDevice = findViewById(R.id.notifications_app_icon_send_to_device_button);
|
||||
|
||||
loadingView = findViewById(R.id.notifications_app_icon_apps_loading);
|
||||
|
||||
appsListView = findViewById(R.id.notifications_app_icon_apps_list);
|
||||
appsListView.setLayoutManager(new LinearLayoutManager(this));
|
||||
|
||||
loadingView.setVisibility(View.VISIBLE);
|
||||
|
||||
loadAppsList();
|
||||
|
||||
uploadToDevice.setOnClickListener(view -> {
|
||||
List<String> selected = appsAdapter.getSelectedItems();
|
||||
if(selected.isEmpty()) {
|
||||
Snackbar.make(appsListView, getString(R.string.notifications_app_icon_nothing_selected), Snackbar.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.notifications_app_icon_upload_to_device)
|
||||
.setMessage(this.getString(R.string.notifications_app_icon_uploading_confirm_description, selected.size()))
|
||||
.setIcon(R.drawable.ic_info)
|
||||
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
|
||||
HashSet<String> iconsToUpload = new HashSet<>(appsAdapter.getSelectedItems());
|
||||
SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(mGBDevice.getAddress());
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
Prefs.putStringSet(editor, DeviceSettingsPreferenceConst.PREF_UPLOAD_NOTIFICATIONS_APP_ICON, iconsToUpload);
|
||||
editor.apply();
|
||||
GBApplication.deviceService(mGBDevice).onSendConfiguration(DeviceSettingsPreferenceConst.PREF_UPLOAD_NOTIFICATIONS_APP_ICON);
|
||||
Snackbar.make(appsListView, getString(R.string.notifications_app_icon_uploading), Snackbar.LENGTH_LONG).show();
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.show();
|
||||
});
|
||||
|
||||
final SearchView searchView = findViewById(R.id.notifications_app_icon_send_to_device_search_view);
|
||||
searchView.setIconifiedByDefault(false);
|
||||
searchView.setIconified(false);
|
||||
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
|
||||
@Override
|
||||
public boolean onQueryTextSubmit(final String query) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextChange(final String newText) {
|
||||
if(appsAdapter != null) {
|
||||
appsAdapter.getFilter().filter(newText);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
void loadAppsList() {
|
||||
new Thread(() -> {
|
||||
final Map<String,String> appNames = new HashMap<>();
|
||||
final List<String> apps = NotificationUtils.getAllApplications(GBApplication.getContext());
|
||||
for(String packageName: apps) {
|
||||
String appName = NotificationUtils.getApplicationLabel(GBApplication.getContext(), packageName);
|
||||
if(!TextUtils.isEmpty(appName)) {
|
||||
appNames.put(packageName, appName);
|
||||
}
|
||||
}
|
||||
Collections.sort(apps, (i1, i2) -> {
|
||||
final String s1 = appNames.get(i1);
|
||||
final String s2 = appNames.get(i2);
|
||||
if(s1 != null && s2 != null)
|
||||
return s1.compareToIgnoreCase(s2);
|
||||
return 0;
|
||||
});
|
||||
runOnUiThread(() -> {
|
||||
SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(mGBDevice.getAddress());
|
||||
HashSet<String> iconsToUpload = (HashSet<String>) prefs.getStringSet(DeviceSettingsPreferenceConst.PREF_UPLOAD_NOTIFICATIONS_APP_ICON, null);
|
||||
if (iconsToUpload == null) {
|
||||
iconsToUpload = new HashSet<>();
|
||||
}
|
||||
appsAdapter = new NotificationsAppIconAdapter(getApplicationContext(), apps, new ArrayList<>(iconsToUpload));
|
||||
appsListView.setAdapter(appsAdapter);
|
||||
loadingView.setVisibility(View.GONE);
|
||||
});
|
||||
}).start();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
|
||||
final int itemId = item.getItemId();
|
||||
if (itemId == android.R.id.home) {
|
||||
// Simulate a back press, so that we don't actually exit the activity when
|
||||
// in a nested PreferenceScreen
|
||||
this.getOnBackPressedDispatcher().onBackPressed();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
+2
@@ -293,6 +293,8 @@ public class DeviceSettingsPreferenceConst {
|
||||
|
||||
public static final String PREF_MUSIC_MANAGEMENT = "pref_music_management";
|
||||
|
||||
public static final String PREF_UPLOAD_NOTIFICATIONS_APP_ICON = "pref_upload_notifications_app_icon";
|
||||
|
||||
public static final String PREF_ANTILOST_ENABLED = "pref_antilost_enabled";
|
||||
public static final String PREF_HYDRATION_SWITCH = "pref_hydration_switch";
|
||||
public static final String PREF_HYDRATION_PERIOD = "pref_hydration_period";
|
||||
|
||||
+14
@@ -67,6 +67,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.AbstractPreferenceFragmen
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.CalBlacklistActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureContacts;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureWorldClocks;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.NotificationsAppIconUploadActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.app_specific_notifications.AppSpecificNotificationSettingsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.audiorecordings.AudioRecordingsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsActivity;
|
||||
@@ -1130,6 +1131,19 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
|
||||
});
|
||||
}
|
||||
|
||||
final Preference notifications_app_icon_upload = findPreference(PREF_UPLOAD_NOTIFICATIONS_APP_ICON);
|
||||
if (notifications_app_icon_upload != null) {
|
||||
notifications_app_icon_upload.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
final Intent intent = new Intent(getContext(), NotificationsAppIconUploadActivity.class);
|
||||
intent.putExtra(GBDevice.EXTRA_DEVICE, device);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final Preference widgets = findPreference(PREF_WIDGETS);
|
||||
if (widgets != null) {
|
||||
widgets.setOnPreferenceClickListener(preference -> {
|
||||
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
/* Copyright (C) 2025 Me7c7
|
||||
|
||||
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 <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.Filter;
|
||||
import android.widget.Filterable;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.NotificationUtils;
|
||||
|
||||
public class NotificationsAppIconAdapter extends RecyclerView.Adapter<NotificationsAppIconAdapter.NotificationsAppIconViewHolder> implements Filterable {
|
||||
|
||||
public static int MAX_SELECT_COUNT = 10;
|
||||
|
||||
private final Context context;
|
||||
|
||||
private final List<String> appList;
|
||||
|
||||
private final List<String> selectedItems;
|
||||
|
||||
private ApplicationFilter applicationFilter;
|
||||
|
||||
public NotificationsAppIconAdapter(Context context, List<String> appList, List<String> selectedItems) {
|
||||
this.context = context;
|
||||
this.appList = appList;
|
||||
this.selectedItems = selectedItems;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return appList.size();
|
||||
}
|
||||
|
||||
|
||||
public List<String> getSelectedItems() {
|
||||
return selectedItems;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(final NotificationsAppIconAdapter.NotificationsAppIconViewHolder holder, int position) {
|
||||
final String packageName = appList.get(position);
|
||||
|
||||
holder.title.setText(NotificationUtils.getApplicationLabel(context, packageName));
|
||||
holder.icon.setImageDrawable(NotificationUtils.getAppIcon(context, packageName));
|
||||
|
||||
holder.checkbox.setChecked(selectedItems.contains(packageName));
|
||||
|
||||
holder.itemView.setOnClickListener(view -> toggleSelection(packageName));
|
||||
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public NotificationsAppIconAdapter.NotificationsAppIconViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_notifications_app_icon, parent, false);
|
||||
return new NotificationsAppIconAdapter.NotificationsAppIconViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Filter getFilter() {
|
||||
if (applicationFilter == null)
|
||||
applicationFilter = new ApplicationFilter(this, appList);
|
||||
return applicationFilter;
|
||||
}
|
||||
|
||||
private void toggleSelection(String packageName) {
|
||||
if(selectedItems.size() >= MAX_SELECT_COUNT) {
|
||||
return;
|
||||
}
|
||||
if(selectedItems.contains(packageName)) {
|
||||
selectedItems.remove(packageName);
|
||||
} else {
|
||||
selectedItems.add(packageName);
|
||||
}
|
||||
int position = appList.indexOf(packageName);
|
||||
new Handler(context.getMainLooper()).post(() -> notifyItemChanged(position));
|
||||
}
|
||||
|
||||
public static class NotificationsAppIconViewHolder extends RecyclerView.ViewHolder {
|
||||
final CheckBox checkbox;
|
||||
final ImageView icon;
|
||||
final TextView title;
|
||||
|
||||
|
||||
NotificationsAppIconViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
checkbox = itemView.findViewById(R.id.item_notifications_app_icon_checkbox);
|
||||
icon = itemView.findViewById(R.id.item_notifications_app_icon_image);
|
||||
title = itemView.findViewById(R.id.item_notifications_app_icon_title);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class ApplicationFilter extends Filter {
|
||||
|
||||
private final NotificationsAppIconAdapter adapter;
|
||||
private final List<String> originalList;
|
||||
private final List<String> filteredList;
|
||||
|
||||
private ApplicationFilter(NotificationsAppIconAdapter adapter, List<String> originalList) {
|
||||
super();
|
||||
this.originalList = new ArrayList<>(originalList);
|
||||
this.filteredList = new ArrayList<>();
|
||||
this.adapter = adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Filter.FilterResults performFiltering(CharSequence filter) {
|
||||
filteredList.clear();
|
||||
final Filter.FilterResults results = new Filter.FilterResults();
|
||||
|
||||
if (filter == null || filter.length() == 0)
|
||||
filteredList.addAll(originalList);
|
||||
else {
|
||||
final String filterPattern = filter.toString().toLowerCase().trim();
|
||||
|
||||
for (String packageName : originalList) {
|
||||
String name = NotificationUtils.getApplicationLabel(context, packageName);
|
||||
if (TextUtils.isEmpty(name)) {
|
||||
name = packageName;
|
||||
}
|
||||
if (name.toLowerCase().contains(filterPattern) ||
|
||||
(packageName.contains(filterPattern))) {
|
||||
filteredList.add(packageName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results.values = filteredList;
|
||||
results.count = filteredList.size();
|
||||
return results;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void publishResults(CharSequence charSequence, Filter.FilterResults filterResults) {
|
||||
adapter.appList.clear();
|
||||
adapter.appList.addAll((List<String>) filterResults.values);
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+4
-1
@@ -323,6 +323,9 @@ public class HuaweiCoordinator {
|
||||
notifications.add(R.xml.devicesettings_disconnectnotification_noshed);
|
||||
if (supportsDoNotDisturb(device))
|
||||
notifications.add(R.xml.devicesettings_donotdisturb_allday_liftwirst_notwear);
|
||||
if (supportsNotificationsAddIconTimestamp() && device.isConnected()) {
|
||||
notifications.add(R.xml.devicesettings_upload_notifications_app_icon);
|
||||
}
|
||||
|
||||
|
||||
// Workout
|
||||
@@ -736,7 +739,7 @@ public class HuaweiCoordinator {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean supportsNotificationsTimestamp() {
|
||||
public boolean supportsNotificationsAddIconTimestamp() {
|
||||
if (supportsExpandCapability())
|
||||
return supportsExpandCapability(77);
|
||||
return false;
|
||||
|
||||
+2
-2
@@ -521,13 +521,13 @@ public class AsynchronousResponse {
|
||||
} else {
|
||||
try {
|
||||
byte fileId = support.huaweiUploadManager.getFileUploadInfo().getFileId();
|
||||
SendFileUploadComplete sendFileUploadComplete = new SendFileUploadComplete(this.support, fileId);
|
||||
sendFileUploadComplete.doPerform();
|
||||
if (support.huaweiUploadManager.getFileUploadInfo().getFileUploadCallback() != null) {
|
||||
support.huaweiUploadManager.getFileUploadInfo().getFileUploadCallback().onUploadComplete();
|
||||
}
|
||||
//Cleanup
|
||||
support.huaweiUploadManager.setFileUploadInfo(null);
|
||||
SendFileUploadComplete sendFileUploadComplete = new SendFileUploadComplete(this.support, fileId);
|
||||
sendFileUploadComplete.doPerform();
|
||||
} catch (IOException e) {
|
||||
LOG.error("Could not send file upload result request", e);
|
||||
}
|
||||
|
||||
+25
@@ -40,6 +40,7 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
@@ -123,6 +124,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.datasync.HuaweiDataSyncFindDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.datasync.HuaweiDataSyncGoals;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p.HuaweiP2PAppIcon;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p.HuaweiP2PCalendarService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p.HuaweiP2PCannedRepliesService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p.HuaweiP2PTrackService;
|
||||
@@ -926,6 +928,12 @@ public class HuaweiSupportProvider {
|
||||
HuaweiP2PDataDictionarySyncService trackService = new HuaweiP2PDataDictionarySyncService(huaweiP2PManager);
|
||||
trackService.register();
|
||||
}
|
||||
if(getHuaweiCoordinator().supportsNotificationsAddIconTimestamp()) {
|
||||
if (HuaweiP2PAppIcon.getRegisteredInstance(huaweiP2PManager) == null) {
|
||||
HuaweiP2PAppIcon appIconService = new HuaweiP2PAppIcon(huaweiP2PManager);
|
||||
appIconService.register();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(getHuaweiCoordinator().supportsThreeCircle() || getHuaweiCoordinator().supportsThreeCircleLite()) {
|
||||
@@ -1208,6 +1216,9 @@ public class HuaweiSupportProvider {
|
||||
case DeviceSettingsPreferenceConst.PREF_CALENDAR_LOOKAHEAD_DAYS:
|
||||
HuaweiP2PCalendarService.getRegisteredInstance(huaweiP2PManager).restartSynchronization();
|
||||
break;
|
||||
case DeviceSettingsPreferenceConst.PREF_UPLOAD_NOTIFICATIONS_APP_ICON:
|
||||
startUploadNotificationsAppIcons();
|
||||
break;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
GB.toast(context, "Configuration of Huawei device failed", Toast.LENGTH_SHORT, GB.ERROR, e);
|
||||
@@ -1267,6 +1278,19 @@ public class HuaweiSupportProvider {
|
||||
}
|
||||
}
|
||||
|
||||
public void startUploadNotificationsAppIcons() {
|
||||
SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress());
|
||||
HashSet<String> iconsToUpload = (HashSet<String>) prefs.getStringSet(DeviceSettingsPreferenceConst.PREF_UPLOAD_NOTIFICATIONS_APP_ICON, null);
|
||||
if (iconsToUpload == null) {
|
||||
iconsToUpload = new HashSet<>();
|
||||
}
|
||||
LOG.debug("startUploadNotificationsAppIcons: {}", iconsToUpload);
|
||||
HuaweiP2PAppIcon appIconService = HuaweiP2PAppIcon.getRegisteredInstance(this.huaweiP2PManager);
|
||||
if(appIconService != null) {
|
||||
appIconService.addPackageName(new ArrayList<>(iconsToUpload));
|
||||
}
|
||||
}
|
||||
|
||||
public void onFetchRecordedData(int dataTypes) {
|
||||
for (int i = 1; i > -1; i <<= 1) {
|
||||
if ((dataTypes & i) != 0) {
|
||||
@@ -2992,4 +3016,5 @@ public class HuaweiSupportProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+422
@@ -0,0 +1,422 @@
|
||||
/* Copyright (C) 2024 Me7c7
|
||||
|
||||
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 <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p;
|
||||
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.core.content.pm.PackageInfoCompat;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiP2PManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiUploadManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendFileUploadInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.NotificationUtils;
|
||||
|
||||
public class HuaweiP2PAppIcon extends HuaweiBaseP2PService {
|
||||
private final Logger LOG = LoggerFactory.getLogger(HuaweiP2PAppIcon.class);
|
||||
|
||||
private final Queue<String> queue = new LinkedList<>();
|
||||
private String currentPackage;
|
||||
|
||||
public static final String MODULE = "hw.unitedevice.notificationpushapp";
|
||||
|
||||
public HuaweiP2PAppIcon(HuaweiP2PManager manager) {
|
||||
super(manager);
|
||||
LOG.info("HuaweiP2PAppIcon");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModule() {
|
||||
return HuaweiP2PAppIcon.MODULE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPackage() {
|
||||
return "in.huawei.NotificationAppIcon";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFingerprint() {
|
||||
return "SystemApp";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registered() {
|
||||
LOG.info("HuaweiP2PAppIcon registered");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregister() {}
|
||||
|
||||
public static HuaweiP2PAppIcon getRegisteredInstance(HuaweiP2PManager manager) {
|
||||
return (HuaweiP2PAppIcon) manager.getRegisteredService(HuaweiP2PAppIcon.MODULE);
|
||||
}
|
||||
|
||||
public void sendMsgToDevice(int type, JSONObject body) throws UnsupportedEncodingException, JSONException {
|
||||
JSONObject msg = new JSONObject();
|
||||
msg.put("msgType", type);
|
||||
msg.put("msgBody", body);
|
||||
|
||||
LOG.info("HuaweiP2PAppIcon sendMsgToDevice: {}", msg);
|
||||
|
||||
sendCommand(msg.toString().getBytes(StandardCharsets.UTF_8), new HuaweiP2PCallback() {
|
||||
@Override
|
||||
public void onResponse(int code, byte[] data) {
|
||||
LOG.info("HuaweiP2PAppIcon sendCommand onResponse: {}", code);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public void sendResponse(String appPkgName, String appInfoHash, int retCode) {
|
||||
try {
|
||||
JSONObject body = new JSONObject();
|
||||
body.put("appPkgName", appPkgName);
|
||||
body.put("retCode", retCode);
|
||||
body.put("appInfoHash", appInfoHash);
|
||||
sendMsgToDevice(6002, body);
|
||||
} catch (UnsupportedEncodingException | JSONException e) {
|
||||
LOG.error("HuaweiP2PAppIcon sendResponse error", e);
|
||||
}
|
||||
}
|
||||
|
||||
private int getType(int retCode) {
|
||||
switch (retCode) {
|
||||
case 10001:
|
||||
return 1;
|
||||
case 10002:
|
||||
return 2;
|
||||
case 10003:
|
||||
return 3;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
//Not sure
|
||||
LOG.warn("HuaweiP2PAppIcon getType default retCode: {}", retCode);
|
||||
return 2;
|
||||
}
|
||||
|
||||
private String getSendFileName(String appPkgName, String appInfoHash, int retCode) {
|
||||
return (appInfoHash + "_" + appPkgName.replace(".", "") + "_" + getType(retCode)) + ".bin";
|
||||
}
|
||||
|
||||
// BitmapUtil.convertDrawableToBitmap BitmapUtil.toBitmap do the same but without opacity and width|height check.
|
||||
// Maybe it is possible to change methods in the BitmapUtil.
|
||||
public Bitmap drawableToBitmap(Drawable drawable) {
|
||||
if (drawable instanceof BitmapDrawable) {
|
||||
return ((BitmapDrawable) drawable).getBitmap();
|
||||
}
|
||||
|
||||
int width = drawable.getIntrinsicWidth();
|
||||
int height = drawable.getIntrinsicHeight();
|
||||
if (width <= 0 || height <= 0) {
|
||||
height = 0;
|
||||
width = 0;
|
||||
}
|
||||
try {
|
||||
Bitmap bitmap = Bitmap.createBitmap(width, height, drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565);
|
||||
drawable.setBounds(0, 0, width, height);
|
||||
drawable.draw(new Canvas(bitmap));
|
||||
return bitmap;
|
||||
} catch (IllegalArgumentException | OutOfMemoryError e) {
|
||||
LOG.error("HuaweiP2PAppIcon drawableToBitmap error", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] convertImageData(Bitmap bmp) {
|
||||
if (bmp == null) {
|
||||
LOG.error("HuaweiP2PAppIcon convertImageData bmp is null");
|
||||
return null;
|
||||
}
|
||||
|
||||
int width = bmp.getWidth();
|
||||
int height = bmp.getHeight();
|
||||
|
||||
int imageSize = Math.multiplyExact(width, height);
|
||||
|
||||
ByteBuffer data = ByteBuffer.allocate(imageSize * 3 + 8);
|
||||
data.order(ByteOrder.LITTLE_ENDIAN);
|
||||
data.putShort((short) 0x2345);
|
||||
data.putShort((short) 0x8888);
|
||||
data.putShort((short) width);
|
||||
data.putShort((short) height);
|
||||
|
||||
int[] imageData = new int[imageSize];
|
||||
bmp.getPixels(imageData, 0, width, 0, 0, width, height);
|
||||
|
||||
int i = 1;
|
||||
for (int j = 0; j < imageData.length; j++) {
|
||||
if (j == imageData.length - 1 || imageData[j] != imageData[j + 1]) {
|
||||
if (i < 4) {
|
||||
for (int k = 0; k < i; k++) {
|
||||
data.putInt(imageData[j]);
|
||||
}
|
||||
} else {
|
||||
data.putInt(0x23456789); //591751049
|
||||
data.putInt(imageData[j]);
|
||||
data.putInt(i);
|
||||
}
|
||||
i = 1;
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return Arrays.copyOfRange(data.array(), 0, data.position());
|
||||
}
|
||||
|
||||
public int convertRetCode(int retCode) {
|
||||
switch (retCode) {
|
||||
case 10001:
|
||||
return 10006;
|
||||
case 10002:
|
||||
return 10007;
|
||||
case 10003:
|
||||
return 10008;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// Not sure
|
||||
LOG.warn("HuaweiP2PAppIcon convertRetCode default retCode: {}", retCode);
|
||||
return 10007;
|
||||
}
|
||||
|
||||
private void uploadImageToDevice(int retCode, String appPkgName, String appInfoHash, byte[] data, String filename) {
|
||||
|
||||
if (data == null) {
|
||||
LOG.error("HuaweiP2PAppIcon uploadImageToDevice no data: {}", appPkgName);
|
||||
return;
|
||||
}
|
||||
HuaweiUploadManager.FileUploadInfo fileInfo = new HuaweiUploadManager.FileUploadInfo();
|
||||
|
||||
fileInfo.setFileType((byte) 7);
|
||||
fileInfo.setFileName(filename);
|
||||
fileInfo.setBytes(data);
|
||||
fileInfo.setSrcPackage(this.getModule());
|
||||
fileInfo.setDstPackage(this.getPackage());
|
||||
fileInfo.setSrcFingerprint(this.getLocalFingerprint());
|
||||
fileInfo.setDstFingerprint(this.getFingerprint());
|
||||
|
||||
fileInfo.setFileUploadCallback(new HuaweiUploadManager.FileUploadCallback() {
|
||||
@Override
|
||||
public void onUploadStart() {
|
||||
manager.getSupportProvider().getDevice().setBusyTask(manager.getSupportProvider().getContext().getString(R.string.updating_firmware));
|
||||
manager.getSupportProvider().getDevice().sendDeviceUpdateIntent(manager.getSupportProvider().getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUploadProgress(int progress) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUploadComplete() {
|
||||
LOG.info("HuaweiP2PAppIcon upload complete");
|
||||
if (manager.getSupportProvider().getDevice().isBusy()) {
|
||||
manager.getSupportProvider().getDevice().unsetBusyTask();
|
||||
manager.getSupportProvider().getDevice().sendDeviceUpdateIntent(manager.getSupportProvider().getContext());
|
||||
}
|
||||
sendResponse(appPkgName, appInfoHash, convertRetCode(retCode));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int code) {
|
||||
LOG.info("HuaweiP2PAppIcon upload error");
|
||||
if (manager.getSupportProvider().getDevice().isBusy()) {
|
||||
manager.getSupportProvider().getDevice().unsetBusyTask();
|
||||
manager.getSupportProvider().getDevice().sendDeviceUpdateIntent(manager.getSupportProvider().getContext());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
HuaweiUploadManager huaweiUploadManager = this.manager.getSupportProvider().getUploadManager();
|
||||
|
||||
huaweiUploadManager.setFileUploadInfo(fileInfo);
|
||||
|
||||
try {
|
||||
SendFileUploadInfo sendFileUploadInfo = new SendFileUploadInfo(this.manager.getSupportProvider(), huaweiUploadManager);
|
||||
sendFileUploadInfo.doPerform();
|
||||
} catch (IOException e) {
|
||||
LOG.error("HuaweiP2PAppIcon Failed to send file upload info", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void deviceRequestIcon(JSONObject jSONObject) throws JSONException {
|
||||
int retCode = jSONObject.getInt("retCode");
|
||||
String appPkgName = jSONObject.getString("appPkgName");
|
||||
LOG.debug("HuaweiP2PAppIcon deviceRequestIcon retCode: {}", retCode);
|
||||
if (retCode == 10004 || retCode == 10005) {
|
||||
resetCurrentPackage();
|
||||
processNext();
|
||||
return;
|
||||
}
|
||||
String appInfoHash = jSONObject.getString("appInfoHash");
|
||||
if (retCode == 10001 || retCode == 10002 || retCode == 10003) {
|
||||
int width = jSONObject.getInt("iconWidth");
|
||||
int height = jSONObject.getInt("iconHeight");
|
||||
LOG.info("HuaweiP2PAppIcon deviceRequestIcon Icon appPkgName: {} iconWidth: {} iconHeight: {}", appPkgName, width, height);
|
||||
if (TextUtils.isEmpty(appPkgName) || width <= 0 || height <= 0) {
|
||||
sendResponse(appPkgName, appInfoHash, 10010);
|
||||
return;
|
||||
}
|
||||
final Drawable drawable = NotificationUtils.getAppIcon(this.manager.getSupportProvider().getContext(), appPkgName);
|
||||
if (drawable == null) {
|
||||
sendResponse(appPkgName, appInfoHash, 10009);
|
||||
LOG.info("HuaweiP2PAppIcon deviceRequestIcon drawable is null. {}", appPkgName);
|
||||
return;
|
||||
}
|
||||
final Bitmap bitmap = drawableToBitmap(drawable);
|
||||
if (bitmap == null) {
|
||||
sendResponse(appPkgName, appInfoHash, 10009);
|
||||
LOG.info("HuaweiP2PAppIcon deviceRequestIcon bitmap is null. {}", appPkgName);
|
||||
return;
|
||||
}
|
||||
final Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, width, height, true);
|
||||
final String filename = getSendFileName(appPkgName, appInfoHash, retCode);
|
||||
|
||||
final byte[] data = convertImageData(scaledBitmap);
|
||||
|
||||
uploadImageToDevice(retCode, appPkgName, appInfoHash, data, filename);
|
||||
return;
|
||||
}
|
||||
sendResponse(appPkgName, appInfoHash, 10010);
|
||||
}
|
||||
|
||||
|
||||
public void deviceStop(JSONObject jSONObject) throws JSONException {
|
||||
int retCode = jSONObject.getInt("retCode");
|
||||
LOG.debug("HuaweiP2PAppIcon deviceStop appPkgName: {} retCode: {}", jSONObject.getString("appPkgName"), retCode);
|
||||
if (retCode == 10011) {
|
||||
resetCurrentPackage();
|
||||
processNext();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleData(byte[] data) {
|
||||
String str = new String(data, StandardCharsets.UTF_8);
|
||||
if (TextUtils.isEmpty(str)) {
|
||||
LOG.error("HuaweiP2PAppIcon data is empty");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
JSONObject jsonData = new JSONObject(str);
|
||||
int msgType = jsonData.getInt("msgType");
|
||||
LOG.debug("HuaweiP2PAppIcon msgType:{} json: {}", msgType, jsonData);
|
||||
JSONObject msgBody = jsonData.getJSONObject("msgBody");
|
||||
if (msgType == 6001) {
|
||||
deviceRequestIcon(msgBody);
|
||||
} else if (msgType == 6002) {
|
||||
deviceStop(msgBody);
|
||||
} else {
|
||||
LOG.info("HuaweiP2PAppIcon msgType is unknown or not implemented");
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
LOG.error("HuaweiP2PAppIcon Error parse response", e);
|
||||
}
|
||||
}
|
||||
|
||||
public PackageInfo getPackageInfo(final String packageName) {
|
||||
try {
|
||||
return GBApplication.getContext().getPackageManager().getPackageInfo(packageName, 0);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
LOG.error("HuaweiP2PAppIcon getPackageInfo NameNotFoundException", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void addPackageName(List<String> packageName) {
|
||||
synchronized (this) {
|
||||
this.queue.addAll(packageName);
|
||||
}
|
||||
processNext();
|
||||
}
|
||||
|
||||
private void processNext() {
|
||||
LOG.info("HuaweiP2PAppIcon processNext");
|
||||
synchronized (this) {
|
||||
if (this.queue.isEmpty() || !TextUtils.isEmpty(currentPackage)) {
|
||||
return;
|
||||
}
|
||||
currentPackage = this.queue.poll();
|
||||
}
|
||||
processUpload(currentPackage);
|
||||
}
|
||||
|
||||
private void resetCurrentPackage() {
|
||||
synchronized (this) {
|
||||
currentPackage = null;
|
||||
}
|
||||
}
|
||||
|
||||
private String getAppHashInfo(final String packageName, final String versionCode) {
|
||||
int versionHash = (packageName + versionCode).hashCode();
|
||||
ByteBuffer buf = ByteBuffer.allocate(4);
|
||||
buf.putInt(versionHash);
|
||||
return GB.hexdump(buf.array()).toLowerCase();
|
||||
}
|
||||
|
||||
private void processUpload(String packageName) {
|
||||
|
||||
PackageInfo packageInfo = getPackageInfo(packageName);
|
||||
String name = NotificationUtils.getApplicationLabel(this.manager.getSupportProvider().getContext(), packageName);
|
||||
if (packageInfo == null || TextUtils.isEmpty(name)) {
|
||||
LOG.error("HuaweiP2PAppIcon error get packageInfo is null. {}", packageName);
|
||||
resetCurrentPackage();
|
||||
processNext();
|
||||
return;
|
||||
}
|
||||
|
||||
String versionCode = String.valueOf(PackageInfoCompat.getLongVersionCode(packageInfo));
|
||||
|
||||
try {
|
||||
String appInfoHash = getAppHashInfo(packageName, versionCode);
|
||||
JSONObject body = new JSONObject();
|
||||
body.put("appInfoHash", appInfoHash);
|
||||
body.put("appPkgName", packageName);
|
||||
body.put("appName", name);
|
||||
body.put("retCode", 10000);
|
||||
sendMsgToDevice(6001, body);
|
||||
} catch (UnsupportedEncodingException | JSONException e) {
|
||||
LOG.error("HuaweiP2PAppIcon processUpload Exception", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -86,7 +86,7 @@ public class SendNotificationRequest extends Request {
|
||||
params.supportsRepeatedNotify = supportProvider.getHuaweiCoordinator().supportsNotificationsRepeatedNotify();
|
||||
params.supportsRemoveSingle = supportProvider.getHuaweiCoordinator().supportsNotificationsRemoveSingle();
|
||||
params.supportsReplyActions = supportProvider.getHuaweiCoordinator().supportsNotificationsReplyActions();
|
||||
params.supportsTimestamp = supportProvider.getHuaweiCoordinator().supportsNotificationsTimestamp();
|
||||
params.supportsTimestamp = supportProvider.getHuaweiCoordinator().supportsNotificationsAddIconTimestamp();
|
||||
|
||||
params.notificationId = notificationSpec.getId();
|
||||
params.notificationKey = getNotificationKey(notificationSpec);
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.widget.RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/notifications_app_icon_info_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="5dp"
|
||||
android:text="@string/notifications_app_icon_info" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/notifications_app_icon_send_to_device_button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/notifications_app_icon_info_text"
|
||||
android:layout_margin="5dp"
|
||||
android:text="@string/notifications_app_icon_upload_to_device" />
|
||||
|
||||
<androidx.appcompat.widget.SearchView
|
||||
android:id="@+id/notifications_app_icon_send_to_device_search_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/notifications_app_icon_send_to_device_button"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingStart="16dp" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/notifications_app_icon_apps_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_below="@id/notifications_app_icon_send_to_device_search_view"
|
||||
android:divider="@null" />
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/notifications_app_icon_apps_loading"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#95000000"
|
||||
android:gravity="center">
|
||||
|
||||
<ProgressBar
|
||||
style="?android:attr/progressBarStyleLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
</RelativeLayout>
|
||||
|
||||
</android.widget.RelativeLayout>
|
||||
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/appmanager_item_card_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
app:cardBackgroundColor="?attr/cardview_background_color"
|
||||
app:cardElevation="3dp"
|
||||
app:contentPadding="8dp">
|
||||
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/activatedBackgroundIndicator"
|
||||
android:minHeight="60dp">
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/item_notifications_app_icon_checkbox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:clickable="false" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/item_notifications_app_icon_image"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_toEndOf="@+id/item_notifications_app_icon_checkbox"
|
||||
android:contentDescription="@string/candidate_item_device_image"
|
||||
android:src="@drawable/ic_music_item"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignWithParentIfMissing="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_toEndOf="@+id/item_notifications_app_icon_image"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/item_notifications_app_icon_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="1"
|
||||
android:scrollHorizontally="false"
|
||||
android:text="Item Name"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
@@ -3919,4 +3919,12 @@
|
||||
<string name="pref_display_enabled_summary">Enable the display on the ring</string>
|
||||
<string name="pref_display_enabled_all_day_title">Enabled all day</string>
|
||||
<string name="pref_display_enabled_all_day_summary">Enable the display the whole day. Disable to select specific times below</string>
|
||||
<string name="notifications_app_icon_upload">Upload Notification App Icon</string>
|
||||
<string name="notifications_app_icon_info">Select applications and press Upload to device. Any previously uploaded icons may be replaced. Current device limitations unknown.</string>
|
||||
<string name="notifications_app_icon_upload_to_device">Upload to device</string>
|
||||
<string name="notifications_app_icon_nothing_selected">Select at least one app to upload</string>
|
||||
<string name="notifications_app_icon_uploading">The icons will be uploaded in the background. You can close this activity</string>
|
||||
<string name="notifications_app_icon_uploading_confirm_description">Are you sure you want to upload %1$d app icons?</string>
|
||||
<string name="pref_notifications_app_icon_upload_title">Upload notification app icons</string>
|
||||
<string name="pref_notifications_app_icon_upload_summary">Upload notification app icons to device</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<Preference
|
||||
android:icon="@drawable/ic_action_manage_apps"
|
||||
android:key="pref_upload_notifications_app_icon"
|
||||
android:summary="@string/pref_notifications_app_icon_upload_summary"
|
||||
android:title="@string/pref_notifications_app_icon_upload_title" />
|
||||
</androidx.preference.PreferenceScreen>
|
||||
Reference in New Issue
Block a user