Huawei: Ability to upload notification app icons

This commit is contained in:
Me7c7
2025-05-01 22:52:34 +00:00
committed by José Rebelo
parent 74ccdb807a
commit e10f3f499c
14 changed files with 947 additions and 4 deletions
+4
View File
@@ -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"
@@ -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);
}
}
@@ -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";
@@ -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 -> {
@@ -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();
}
}
}
@@ -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;
@@ -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);
}
@@ -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 {
}
}
}
}
@@ -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);
}
}
}
@@ -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>
+8
View File
@@ -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>