mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-27 20:36:51 +01:00
Fossil Hybrid HR: Add watchface designer
This commit is contained in:
parent
bcc5afb78c
commit
de403cf92e
@ -618,6 +618,10 @@
|
||||
android:name=".devices.qhybrid.CommuteActionsActivity"
|
||||
android:label="@string/qhybrid_pref_title_actions"
|
||||
android:parentActivityName=".devices.qhybrid.HRConfigActivity" />
|
||||
<activity
|
||||
android:name=".devices.qhybrid.HybridHRWatchfaceDesignerActivity"
|
||||
android:label="Watchface designer"
|
||||
android:parentActivityName=".activities.appmanager.AppManagerActivity" />
|
||||
<activity
|
||||
android:name=".devices.um25.Activity.DataActivity"
|
||||
android:exported="true" />
|
||||
|
7
app/src/main/assets/fossil_hr/icWthClearDay
Normal file
7
app/src/main/assets/fossil_hr/icWthClearDay
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#<0C><>
|
7
app/src/main/assets/fossil_hr/icWthClearNite
Normal file
7
app/src/main/assets/fossil_hr/icWthClearNite
Normal file
@ -0,0 +1,7 @@
|
||||
%
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
7<0C><>
|
1
app/src/main/assets/fossil_hr/icWthCloudy
Normal file
1
app/src/main/assets/fossil_hr/icWthCloudy
Normal file
@ -0,0 +1 @@
|
||||
h
b<0C><>
|
2
app/src/main/assets/fossil_hr/icWthPartCloudyDay
Normal file
2
app/src/main/assets/fossil_hr/icWthPartCloudyDay
Normal file
@ -0,0 +1,2 @@
|
||||
&
|
||||
2<0C><>
|
2
app/src/main/assets/fossil_hr/icWthPartCloudyNite
Normal file
2
app/src/main/assets/fossil_hr/icWthPartCloudyNite
Normal file
@ -0,0 +1,2 @@
|
||||
#
|
||||
2<0C><>
|
1
app/src/main/assets/fossil_hr/icWthRainy
Normal file
1
app/src/main/assets/fossil_hr/icWthRainy
Normal file
@ -0,0 +1 @@
|
||||
8
<<0C><>
|
1
app/src/main/assets/fossil_hr/icWthSnowy
Normal file
1
app/src/main/assets/fossil_hr/icWthSnowy
Normal file
@ -0,0 +1 @@
|
||||
<0C><>
|
2
app/src/main/assets/fossil_hr/icWthStormy
Normal file
2
app/src/main/assets/fossil_hr/icWthStormy
Normal file
@ -0,0 +1,2 @@
|
||||
9
|
||||
53<0C><>
|
2
app/src/main/assets/fossil_hr/icWthWindy
Normal file
2
app/src/main/assets/fossil_hr/icWthWindy
Normal file
@ -0,0 +1,2 @@
|
||||
8
|
||||
53<0C><>
|
BIN
app/src/main/assets/fossil_hr/openSourceWatchface
Normal file
BIN
app/src/main/assets/fossil_hr/openSourceWatchface
Normal file
Binary file not shown.
BIN
app/src/main/assets/fossil_hr/widgetDate
Normal file
BIN
app/src/main/assets/fossil_hr/widgetDate
Normal file
Binary file not shown.
BIN
app/src/main/assets/fossil_hr/widgetWeather
Normal file
BIN
app/src/main/assets/fossil_hr/widgetWeather
Normal file
Binary file not shown.
@ -17,6 +17,7 @@
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.appmanager;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@ -249,8 +250,6 @@ public abstract class AbstractAppManagerFragment extends Fragment {
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
mGBDevice = ((AppManagerActivity) getActivity()).getGBDevice();
|
||||
mCoordinator = DeviceHelper.getInstance().getCoordinator(mGBDevice);
|
||||
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(ACTION_REFRESH_APPLIST);
|
||||
@ -269,8 +268,12 @@ public abstract class AbstractAppManagerFragment extends Fragment {
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
mGBDevice = ((AppManagerActivity) getActivity()).getGBDevice();
|
||||
mCoordinator = DeviceHelper.getInstance().getCoordinator(mGBDevice);
|
||||
|
||||
final FloatingActionButton appListFab = ((FloatingActionButton) getActivity().findViewById(R.id.fab));
|
||||
final FloatingActionButton appListFabNew = ((FloatingActionButton) getActivity().findViewById(R.id.fab_new));
|
||||
final Class<? extends Activity> watchfaceDesignerActivity = mCoordinator.getWatchfaceDesignerActivity();
|
||||
View rootView = inflater.inflate(R.layout.activity_appmanager, container, false);
|
||||
|
||||
RecyclerView appListView = (RecyclerView) (rootView.findViewById(R.id.appListView));
|
||||
@ -280,8 +283,12 @@ public abstract class AbstractAppManagerFragment extends Fragment {
|
||||
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
|
||||
if (dy > 0) {
|
||||
appListFab.hide();
|
||||
appListFabNew.hide();
|
||||
} else if (dy < 0) {
|
||||
appListFab.show();
|
||||
if (watchfaceDesignerActivity != null) {
|
||||
appListFabNew.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -293,6 +300,19 @@ public abstract class AbstractAppManagerFragment extends Fragment {
|
||||
appManagementTouchHelper = new ItemTouchHelper(appItemTouchHelperCallback);
|
||||
|
||||
appManagementTouchHelper.attachToRecyclerView(appListView);
|
||||
|
||||
if ((watchfaceDesignerActivity != null) && (appListFabNew != null)) {
|
||||
appListFabNew.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent startIntent = new Intent(getContext(), watchfaceDesignerActivity);
|
||||
startIntent.putExtra(GBDevice.EXTRA_DEVICE, mGBDevice);
|
||||
getContext().startActivity(startIntent);
|
||||
}
|
||||
});
|
||||
appListFabNew.show();
|
||||
}
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
@ -385,7 +405,7 @@ public abstract class AbstractAppManagerFragment extends Fragment {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.appmanager_app_delete_cache:
|
||||
String baseName = selectedApp.getUUID().toString();
|
||||
String[] suffixToDelete = new String[]{mCoordinator.getAppFileExtension(), ".json", "_config.js", "_preset.json"};
|
||||
String[] suffixToDelete = new String[]{mCoordinator.getAppFileExtension(), ".json", "_config.js", "_preset.json", ".png"};
|
||||
for (String suffix : suffixToDelete) {
|
||||
File fileToDelete = new File(appCacheDir,baseName + suffix);
|
||||
if (!fileToDelete.delete()) {
|
||||
|
@ -51,7 +51,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
|
||||
public class AppManagerActivity extends AbstractGBFragmentActivity {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AbstractAppManagerFragment.class);
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AppManagerActivity.class);
|
||||
private int READ_REQUEST_CODE = 42;
|
||||
|
||||
private GBDevice mGBDevice = null;
|
||||
|
@ -174,6 +174,12 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Class<? extends Activity> getWatchfaceDesignerActivity() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBondingStyle() {
|
||||
return BONDING_STYLE_ASK;
|
||||
|
@ -244,6 +244,13 @@ public interface DeviceCoordinator {
|
||||
*/
|
||||
Class<? extends Activity> getAppsManagementActivity();
|
||||
|
||||
/**
|
||||
* Returns the Activity class that will be used to design watchfaces.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
Class<? extends Activity> getWatchfaceDesignerActivity();
|
||||
|
||||
/**
|
||||
* Returns the device app cache directory.
|
||||
*/
|
||||
|
@ -0,0 +1,161 @@
|
||||
/* Copyright (C) 2021 Arjan Schrijver, Daniel Dakhno
|
||||
|
||||
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.devices.qhybrid;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.CRC32C;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Writes watch apps to a file in the Fossil Hybrid HR .wapp format.
|
||||
*/
|
||||
public class FossilAppWriter {
|
||||
private final Logger LOG = LoggerFactory.getLogger(FossilAppWriter.class);
|
||||
private Context mContext;
|
||||
private String version;
|
||||
private LinkedHashMap<String, InputStream> code;
|
||||
private LinkedHashMap<String, InputStream> icons;
|
||||
private LinkedHashMap<String, String> layout;
|
||||
private LinkedHashMap<String, String> displayName;
|
||||
private LinkedHashMap<String, String> config;
|
||||
|
||||
public FossilAppWriter(Context context, String version, LinkedHashMap<String, InputStream> code, LinkedHashMap<String, InputStream> icons, LinkedHashMap<String, String> layout, LinkedHashMap<String, String> displayName, LinkedHashMap<String, String> config) {
|
||||
this.mContext = context;
|
||||
if (this.mContext == null) throw new AssertionError("context cannot be null");
|
||||
this.version = version;
|
||||
if (!this.version.matches("^[0-9]\\.[0-9]\\.[0-9]\\.[0-9]$")) throw new AssertionError("Version must be in x.x.x.x format");
|
||||
this.code = code;
|
||||
if (this.code.size() == 0) throw new AssertionError("At least one code file InputStream must be supplied");
|
||||
this.icons = icons;
|
||||
if (this.icons == null) throw new AssertionError("icons cannot be null");
|
||||
this.layout = layout;
|
||||
if (this.layout == null) throw new AssertionError("layout cannot be null");
|
||||
this.displayName = displayName;
|
||||
if (this.displayName == null) throw new AssertionError("displayName cannot be null");
|
||||
this.config = config;
|
||||
if (this.config == null) throw new AssertionError("config cannot be null");
|
||||
}
|
||||
|
||||
public byte[] getWapp() throws IOException {
|
||||
byte[] codeData = loadFiles(code);
|
||||
byte[] iconsData = loadFiles(icons);
|
||||
byte[] layoutData = loadStringFiles(layout);
|
||||
byte[] displayNameData = loadStringFiles(displayName);
|
||||
byte[] configData = loadStringFiles(config);
|
||||
|
||||
int offsetCode = 88;
|
||||
int offsetIcons = offsetCode + codeData.length;
|
||||
int offsetLayout = offsetIcons + iconsData.length;
|
||||
int offsetDisplayName = offsetLayout + layoutData.length;
|
||||
int offsetConfig = offsetDisplayName + displayNameData.length;
|
||||
int offsetFileEnd = offsetConfig + configData.length;
|
||||
|
||||
ByteArrayOutputStream filePart = new ByteArrayOutputStream();
|
||||
String[] versionParts = this.version.split("\\.");
|
||||
for (String versionPart : versionParts) {
|
||||
filePart.write(Integer.valueOf(versionPart).byteValue());
|
||||
}
|
||||
filePart.write(intToLEBytes(0));
|
||||
filePart.write(intToLEBytes(0));
|
||||
filePart.write(intToLEBytes(offsetCode));
|
||||
filePart.write(intToLEBytes(offsetIcons));
|
||||
filePart.write(intToLEBytes(offsetLayout));
|
||||
filePart.write(intToLEBytes(offsetDisplayName));
|
||||
filePart.write(intToLEBytes(offsetDisplayName));
|
||||
filePart.write(intToLEBytes(offsetConfig));
|
||||
filePart.write(intToLEBytes(offsetFileEnd));
|
||||
filePart.write(intToLEBytes(0));
|
||||
filePart.write(intToLEBytes(0));
|
||||
filePart.write(intToLEBytes(0));
|
||||
filePart.write(intToLEBytes(0));
|
||||
filePart.write(intToLEBytes(0));
|
||||
filePart.write(intToLEBytes(0));
|
||||
filePart.write(intToLEBytes(0));
|
||||
filePart.write(intToLEBytes(0));
|
||||
filePart.write(intToLEBytes(0));
|
||||
filePart.write(codeData);
|
||||
filePart.write(iconsData);
|
||||
filePart.write(layoutData);
|
||||
filePart.write(displayNameData);
|
||||
filePart.write(configData);
|
||||
byte[] filePartBytes = filePart.toByteArray();
|
||||
|
||||
ByteArrayOutputStream wapp = new ByteArrayOutputStream();
|
||||
wapp.write(new byte[]{(byte)0xFE, (byte)0x15}); // file handle
|
||||
wapp.write(new byte[]{(byte)0x03, (byte)0x00}); // file version
|
||||
wapp.write(intToLEBytes(0)); // file offset
|
||||
wapp.write(intToLEBytes(filePartBytes.length));
|
||||
wapp.write(filePartBytes);
|
||||
|
||||
CRC32C crc = new CRC32C();
|
||||
crc.update(filePartBytes,0,filePartBytes.length);
|
||||
wapp.write(intToLEBytes((int)crc.getValue()));
|
||||
|
||||
return wapp.toByteArray();
|
||||
}
|
||||
|
||||
public byte[] loadFiles(LinkedHashMap<String, InputStream> filesMap) throws IOException {
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
for (String filename : filesMap.keySet()) {
|
||||
InputStream in = filesMap.get(filename);
|
||||
output.write((byte)filename.length() + 1);
|
||||
output.write(StringUtils.terminateNull(filename).getBytes(StandardCharsets.UTF_8));
|
||||
output.write(shortToLEBytes((short)in.available()));
|
||||
byte[] fileBytes = new byte[in.available()];
|
||||
in.read(fileBytes);
|
||||
output.write(fileBytes);
|
||||
}
|
||||
return output.toByteArray();
|
||||
}
|
||||
|
||||
public byte[] loadStringFiles(LinkedHashMap<String, String> stringsMap) throws IOException {
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
for (String filename : stringsMap.keySet()) {
|
||||
output.write((byte)filename.length() + 1);
|
||||
output.write(StringUtils.terminateNull(filename).getBytes(StandardCharsets.UTF_8));
|
||||
output.write(shortToLEBytes((short)(stringsMap.get(filename).length() + 1)));
|
||||
output.write(StringUtils.terminateNull(stringsMap.get(filename)).getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
return output.toByteArray();
|
||||
}
|
||||
|
||||
private static byte[] intToLEBytes(int number) {
|
||||
byte[] b = new byte[4];
|
||||
b[0] = (byte) (number & 0xFF);
|
||||
b[1] = (byte) ((number >> 8) & 0xFF);
|
||||
b[2] = (byte) ((number >> 16) & 0xFF);
|
||||
b[3] = (byte) ((number >> 24) & 0xFF);
|
||||
return b;
|
||||
}
|
||||
|
||||
private static byte[] shortToLEBytes(short number) {
|
||||
byte[] b = new byte[2];
|
||||
b[0] = (byte) (number & 0xFF);
|
||||
b[1] = (byte) ((number >> 8) & 0xFF);
|
||||
return b;
|
||||
}
|
||||
}
|
@ -43,6 +43,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.UriHelper;
|
||||
*/
|
||||
public class FossilFileReader {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FossilFileReader.class);
|
||||
private Uri uri;
|
||||
private final UriHelper uriHelper;
|
||||
private boolean isValid = false;
|
||||
private boolean isFirmware = false;
|
||||
@ -54,6 +55,7 @@ public class FossilFileReader {
|
||||
private JSONObject mAppKeys;
|
||||
|
||||
public FossilFileReader(Uri uri, Context context) throws IOException {
|
||||
this.uri = uri;
|
||||
uriHelper = UriHelper.get(uri, context);
|
||||
|
||||
try (InputStream in = new BufferedInputStream(uriHelper.openInputStream())) {
|
||||
@ -190,6 +192,10 @@ public class FossilFileReader {
|
||||
return isWatchface;
|
||||
}
|
||||
|
||||
public Uri getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return foundVersion;
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
@ -29,6 +30,7 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
@ -104,6 +106,12 @@ public class FossilHRInstallHandler implements InstallHandler {
|
||||
if (fossilFile.isFirmware()) {
|
||||
return;
|
||||
}
|
||||
saveAppInCache(fossilFile, null, mCoordinator, mContext);
|
||||
// refresh list
|
||||
manager.sendBroadcast(new Intent(AbstractAppManagerFragment.ACTION_REFRESH_APPLIST));
|
||||
}
|
||||
|
||||
public static void saveAppInCache(FossilFileReader fossilFile, Bitmap backgroundImg, DeviceCoordinator mCoordinator, Context mContext) {
|
||||
GBDeviceApp app;
|
||||
File destDir;
|
||||
// write app file
|
||||
@ -111,7 +119,7 @@ public class FossilHRInstallHandler implements InstallHandler {
|
||||
app = fossilFile.getGBDeviceApp();
|
||||
destDir = mCoordinator.getAppCacheDir();
|
||||
destDir.mkdirs();
|
||||
FileUtils.copyURItoFile(mContext, mUri, new File(destDir, app.getUUID().toString() + mCoordinator.getAppFileExtension()));
|
||||
FileUtils.copyURItoFile(mContext, fossilFile.getUri(), new File(destDir, app.getUUID().toString() + mCoordinator.getAppFileExtension()));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Saving app in cache failed: " + e.getMessage(), e);
|
||||
return;
|
||||
@ -140,8 +148,17 @@ public class FossilHRInstallHandler implements InstallHandler {
|
||||
} catch (JSONException e) {
|
||||
LOG.error(e.getMessage(), e);
|
||||
}
|
||||
// refresh list
|
||||
manager.sendBroadcast(new Intent(AbstractAppManagerFragment.ACTION_REFRESH_APPLIST));
|
||||
// write watchface background image
|
||||
if (backgroundImg != null) {
|
||||
outputFile = new File(destDir, app.getUUID().toString() + ".png");
|
||||
try {
|
||||
FileOutputStream fos = new FileOutputStream(outputFile);
|
||||
backgroundImg.compress(Bitmap.CompressFormat.PNG, 9, fos);
|
||||
fos.close();
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to write to output file: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -0,0 +1,375 @@
|
||||
/* Copyright (C) 2021 Arjan Schrijver
|
||||
|
||||
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.devices.qhybrid;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.NumberPicker;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.BitmapUtil;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
||||
|
||||
public class HybridHRWatchfaceDesignerActivity extends AppCompatActivity implements View.OnClickListener {
|
||||
private final Logger LOG = LoggerFactory.getLogger(HybridHRWatchfaceDesignerActivity.class);
|
||||
private GBDevice mGBDevice;
|
||||
private DeviceCoordinator mCoordinator;
|
||||
private int displayImageSize = 0;
|
||||
private float scaleFactor = 0;
|
||||
private ImageView backgroundImageView;
|
||||
private Bitmap selectedBackgroundImage, processedBackgroundImage;
|
||||
private String watchfaceName = "NewWatchface";
|
||||
final private ArrayList<HybridHRWatchfaceWidget> widgets = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_hybridhr_watchface_designer);
|
||||
|
||||
Intent intent = getIntent();
|
||||
Bundle bundle = intent.getExtras();
|
||||
if (bundle != null) {
|
||||
mGBDevice = bundle.getParcelable(GBDevice.EXTRA_DEVICE);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Must provide a device when invoking this activity");
|
||||
}
|
||||
mCoordinator = DeviceHelper.getInstance().getCoordinator(mGBDevice);
|
||||
|
||||
calculateDisplayImageSize();
|
||||
backgroundImageView = findViewById(R.id.hybridhr_background_image);
|
||||
renderWatchfacePreview();
|
||||
|
||||
findViewById(R.id.button_edit_name).setOnClickListener(this);
|
||||
findViewById(R.id.button_set_background).setOnClickListener(this);
|
||||
findViewById(R.id.button_add_widget).setOnClickListener(this);
|
||||
findViewById(R.id.button_watchface_settings).setOnClickListener(this);
|
||||
findViewById(R.id.button_preview_watchface).setOnClickListener(this);
|
||||
findViewById(R.id.button_save_watchface).setOnClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
|
||||
super.onActivityResult(requestCode, resultCode, resultData);
|
||||
if (requestCode == 42 && resultCode == Activity.RESULT_OK) {
|
||||
Uri imageUri = resultData.getData();
|
||||
if (imageUri == null) {
|
||||
LOG.warn("No image selected");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
selectedBackgroundImage = createImageFromURI(imageUri);
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Error converting selected image to Bitmap", e);
|
||||
return;
|
||||
}
|
||||
renderWatchfacePreview();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (v.getId() == R.id.button_edit_name) {
|
||||
final EditText input = new EditText(this);
|
||||
input.setText(watchfaceName);
|
||||
input.setId(0);
|
||||
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.MATCH_PARENT);
|
||||
input.setLayoutParams(lp);
|
||||
new AlertDialog.Builder(this)
|
||||
.setView(input)
|
||||
.setNegativeButton(R.string.fossil_hr_new_action_cancel, null)
|
||||
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
watchfaceName = input.getText().toString();
|
||||
TextView watchfaceNameView = findViewById(R.id.watchface_name);
|
||||
watchfaceNameView.setText(watchfaceName);
|
||||
}
|
||||
})
|
||||
.setTitle("Set watchface name")
|
||||
.show();
|
||||
} else if (v.getId() == R.id.button_set_background) {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("image/*");
|
||||
startActivityForResult(intent, 42);
|
||||
} else if (v.getId() == R.id.button_add_widget) {
|
||||
showWidgetEditPopup(-1);
|
||||
} else if (v.getId() == R.id.button_preview_watchface) {
|
||||
sendToWatch(true);
|
||||
} else if (v.getId() == R.id.button_save_watchface) {
|
||||
sendToWatch(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void renderWatchfacePreview() {
|
||||
int widgetSize = 50;
|
||||
if (selectedBackgroundImage == null) {
|
||||
processedBackgroundImage = Bitmap.createBitmap(displayImageSize, displayImageSize, Bitmap.Config.ARGB_8888);
|
||||
// Paint a gray circle around the watchface
|
||||
Canvas backgroundImageCanvas = new Canvas(processedBackgroundImage);
|
||||
Paint circlePaint = new Paint();
|
||||
circlePaint.setColor(Color.GRAY);
|
||||
circlePaint.setAntiAlias(true);
|
||||
circlePaint.setStrokeWidth(3);
|
||||
circlePaint.setStyle(Paint.Style.STROKE);
|
||||
backgroundImageCanvas.drawCircle(displayImageSize/2f + 2, displayImageSize/2f + 2, displayImageSize/2f - 5, circlePaint);
|
||||
} else {
|
||||
processedBackgroundImage = Bitmap.createScaledBitmap(selectedBackgroundImage, displayImageSize, displayImageSize, false);
|
||||
}
|
||||
// Remove existing widget ImageViews
|
||||
RelativeLayout imageContainer = this.findViewById(R.id.watchface_preview_image);
|
||||
boolean onlyPreviewIsRemaining = false;
|
||||
while (!onlyPreviewIsRemaining) {
|
||||
int childCount = imageContainer.getChildCount();
|
||||
int i;
|
||||
for(i=0; i<childCount; i++) {
|
||||
View currentChild = imageContainer.getChildAt(i);
|
||||
if (currentChild.getId() != R.id.hybridhr_background_image) {
|
||||
imageContainer.removeView(currentChild);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (i == childCount) {
|
||||
onlyPreviewIsRemaining = true;
|
||||
}
|
||||
}
|
||||
// Dynamically add an ImageView for each widget
|
||||
Paint widgetPaint = new Paint();
|
||||
widgetPaint.setColor(Color.RED);
|
||||
widgetPaint.setStyle(Paint.Style.STROKE);
|
||||
widgetPaint.setStrokeWidth(5);
|
||||
Bitmap widgetBitmap = Bitmap.createBitmap((int)(widgetSize * scaleFactor), (int)(widgetSize * scaleFactor), Bitmap.Config.ARGB_8888);
|
||||
Canvas widgetCanvas = new Canvas(widgetBitmap);
|
||||
widgetCanvas.drawRect(0, 0, widgetSize * scaleFactor, widgetSize * scaleFactor, widgetPaint);
|
||||
for (int i=0; i<widgets.size(); i++) {
|
||||
HybridHRWatchfaceWidget widget = widgets.get(i);
|
||||
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
|
||||
layoutParams.addRule(RelativeLayout.ALIGN_LEFT, backgroundImageView.getId());
|
||||
layoutParams.addRule(RelativeLayout.ALIGN_TOP, backgroundImageView.getId());
|
||||
layoutParams.setMargins((int) ((widget.getPosX() - widgetSize/2) * scaleFactor), (int) ((widget.getPosY() - widgetSize/2) * scaleFactor), 0, 0);
|
||||
ImageView widgetView = new ImageView(this);
|
||||
widgetView.setId(i);
|
||||
widgetView.setImageBitmap(widgetBitmap);
|
||||
widgetView.setLayoutParams(layoutParams);
|
||||
widgetView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
showWidgetEditPopup(v.getId());
|
||||
}
|
||||
});
|
||||
imageContainer.addView(widgetView);
|
||||
}
|
||||
backgroundImageView.setImageBitmap(processedBackgroundImage);
|
||||
}
|
||||
|
||||
private void showWidgetEditPopup(final int index) {
|
||||
LinearLayout layout = new LinearLayout(this);
|
||||
HybridHRWatchfaceWidget widget = null;
|
||||
if (index >= 0) {
|
||||
widget = widgets.get(index);
|
||||
}
|
||||
layout.setOrientation(LinearLayout.VERTICAL);
|
||||
TextView desc = new TextView(this);
|
||||
desc.setText("Type:");
|
||||
layout.addView(desc);
|
||||
final RadioGroup typeSelector = new RadioGroup(this);
|
||||
RadioButton typeDate = new RadioButton(this);
|
||||
typeDate.setText("Date");
|
||||
typeDate.setId(0);
|
||||
if ((widget != null) && (widget.getWidgetType().equals("widgetDate"))) {
|
||||
typeDate.setChecked(true);
|
||||
}
|
||||
typeSelector.addView(typeDate);
|
||||
RadioButton typeWeather = new RadioButton(this);
|
||||
typeWeather.setText("Weather");
|
||||
typeWeather.setId(0+1);
|
||||
if ((widget != null) && (widget.getWidgetType().equals("widgetWeather"))) {
|
||||
typeWeather.setChecked(true);
|
||||
}
|
||||
typeSelector.addView(typeWeather);
|
||||
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.MATCH_PARENT);
|
||||
typeSelector.setLayoutParams(lp);
|
||||
layout.addView(typeSelector);
|
||||
desc = new TextView(this);
|
||||
desc.setText("X coordinate:");
|
||||
layout.addView(desc);
|
||||
final NumberPicker posX = new NumberPicker(this);
|
||||
posX.setMinValue(1);
|
||||
posX.setMaxValue(240);
|
||||
if (widget != null) {
|
||||
posX.setValue(widget.getPosX());
|
||||
}
|
||||
layout.addView(posX);
|
||||
desc = new TextView(this);
|
||||
desc.setText("Y coordinate:");
|
||||
layout.addView(desc);
|
||||
final NumberPicker posY = new NumberPicker(this);
|
||||
posY.setMinValue(1);
|
||||
posY.setMaxValue(240);
|
||||
if (widget != null) {
|
||||
posY.setValue(widget.getPosY());
|
||||
}
|
||||
layout.addView(posY);
|
||||
new AlertDialog.Builder(this)
|
||||
.setView(layout)
|
||||
.setNegativeButton(R.string.fossil_hr_edit_action_delete, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
if (index >= 0) {
|
||||
widgets.remove(index);
|
||||
renderWatchfacePreview();
|
||||
}
|
||||
}
|
||||
})
|
||||
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
int selectedRadioId = typeSelector.getCheckedRadioButtonId();
|
||||
int selectedPosX = posX.getValue();
|
||||
int selectedPosY = posY.getValue();
|
||||
switch (selectedRadioId) {
|
||||
case 0:
|
||||
if (index >= 0) {
|
||||
widgets.set(index, new HybridHRWatchfaceWidget("widgetDate", selectedPosX, selectedPosY));
|
||||
} else {
|
||||
widgets.add(new HybridHRWatchfaceWidget("widgetDate", selectedPosX, selectedPosY));
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
if (index >= 0) {
|
||||
widgets.set(index, new HybridHRWatchfaceWidget("widgetWeather", selectedPosX, selectedPosY));
|
||||
} else {
|
||||
widgets.add(new HybridHRWatchfaceWidget("widgetWeather", selectedPosX, selectedPosY));
|
||||
}
|
||||
break;
|
||||
}
|
||||
renderWatchfacePreview();
|
||||
}
|
||||
})
|
||||
.setTitle("Add widget")
|
||||
.show();
|
||||
}
|
||||
|
||||
private void calculateDisplayImageSize() {
|
||||
DisplayMetrics displayMetrics = new DisplayMetrics();
|
||||
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
|
||||
displayImageSize = (int) Math.round(displayMetrics.widthPixels * 0.75);
|
||||
scaleFactor = displayImageSize / 240f;
|
||||
}
|
||||
|
||||
private Bitmap createImageFromURI(Uri imageUri) throws IOException, RuntimeException {
|
||||
if (imageUri == null) {
|
||||
throw new RuntimeException("No image selected");
|
||||
}
|
||||
|
||||
// UriHelper uriHelper = UriHelper.get(imageUri, this);
|
||||
// InputStream in = new BufferedInputStream(uriHelper.openInputStream());
|
||||
// Bitmap bitmap = BitmapFactory.decodeStream(in);
|
||||
|
||||
ContentResolver resolver = getContentResolver();
|
||||
Cursor c = resolver.query(imageUri, new String[]{MediaStore.Images.ImageColumns.ORIENTATION}, null, null, null);
|
||||
c.moveToFirst();
|
||||
int orientation = c.getInt(c.getColumnIndex(MediaStore.Images.ImageColumns.ORIENTATION));
|
||||
c.close();
|
||||
Bitmap bitmap = MediaStore.Images.Media.getBitmap(this.getContentResolver(), imageUri);
|
||||
if (orientation != 0) { // FIXME: doesn't seem to work
|
||||
Matrix matrix = new Matrix();
|
||||
matrix.postRotate(90);
|
||||
bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
|
||||
}
|
||||
|
||||
return BitmapUtil.convertToGrayscale(BitmapUtil.getCircularBitmap(bitmap));
|
||||
}
|
||||
|
||||
private void sendToWatch(boolean preview) {
|
||||
HybridHRWatchfaceFactory wfFactory;
|
||||
if (preview) {
|
||||
wfFactory = new HybridHRWatchfaceFactory("previewWatchface");
|
||||
} else {
|
||||
wfFactory = new HybridHRWatchfaceFactory(watchfaceName);
|
||||
}
|
||||
wfFactory.setBackground(processedBackgroundImage);
|
||||
wfFactory.addWidgets(widgets);
|
||||
try {
|
||||
File tempFile = File.createTempFile("tmpWatchfaceFile", null);
|
||||
tempFile.deleteOnExit();
|
||||
FileOutputStream fos = new FileOutputStream(tempFile);
|
||||
BufferedOutputStream bos = new BufferedOutputStream(fos);
|
||||
bos.write(wfFactory.getWapp(this));
|
||||
bos.close();
|
||||
fos.close();
|
||||
Uri tempAppFileUri = Uri.fromFile(tempFile);
|
||||
GBApplication.deviceService().onInstallApp(tempAppFileUri);
|
||||
if (preview) {
|
||||
new Handler().postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
GBApplication.deviceService().onAppDelete(UUID.nameUUIDFromBytes("previewWatchface".getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
}, 10000);
|
||||
} else {
|
||||
FossilFileReader fossilFile = new FossilFileReader(tempAppFileUri, this);
|
||||
FossilHRInstallHandler.saveAppInCache(fossilFile, processedBackgroundImage, mCoordinator, this);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Error while creating and uploading watchface", e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,265 @@
|
||||
/* Copyright (C) 2021 Arjan Schrijver
|
||||
|
||||
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.devices.qhybrid;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.image.ImageConverter;
|
||||
|
||||
public class HybridHRWatchfaceFactory {
|
||||
private final Logger LOG = LoggerFactory.getLogger(HybridHRWatchfaceFactory.class);
|
||||
private String watchfaceName;
|
||||
private Bitmap background;
|
||||
private ArrayList<JSONObject> widgets = new ArrayList<>();
|
||||
|
||||
public HybridHRWatchfaceFactory(String name) {
|
||||
watchfaceName = name.replaceAll("[^-A-Za-z0-9]", "");
|
||||
if (watchfaceName.equals("")) throw new AssertionError("name cannot be empty");
|
||||
if (watchfaceName.endsWith("App")) watchfaceName += "Watchface";
|
||||
}
|
||||
|
||||
public void setBackground(Bitmap background) {
|
||||
if ((background.getWidth() == 240) && (background.getHeight() == 240)) {
|
||||
this.background = background;
|
||||
} else {
|
||||
this.background = Bitmap.createScaledBitmap(background, 240, 240, true);
|
||||
}
|
||||
}
|
||||
|
||||
public void addWidget(HybridHRWatchfaceWidget widgetDesc) {
|
||||
JSONObject widget = new JSONObject();
|
||||
try {
|
||||
switch (widgetDesc.getWidgetType()) {
|
||||
case "widgetDate":
|
||||
widget.put("type", "comp");
|
||||
widget.put("name", widgetDesc.getWidgetType());
|
||||
widget.put("goal_ring", false);
|
||||
widget.put("color", "white");
|
||||
widget.put("bg", "_00.rle");
|
||||
break;
|
||||
case "widgetWeather":
|
||||
widget.put("type", "comp");
|
||||
widget.put("name", widgetDesc.getWidgetType());
|
||||
widget.put("goal_ring", false);
|
||||
widget.put("color", "white");
|
||||
widget.put("bg", "_01.rle");
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Invalid widget name: " + widgetDesc.getWidgetType());
|
||||
return;
|
||||
}
|
||||
JSONObject size = new JSONObject();
|
||||
size.put("w", 76);
|
||||
size.put("h", 76);
|
||||
widget.put("size", size);
|
||||
JSONObject pos = new JSONObject();
|
||||
pos.put("x", widgetDesc.getPosX());
|
||||
pos.put("y", widgetDesc.getPosY());
|
||||
widget.put("pos", pos);
|
||||
widgets.add(widget);
|
||||
} catch (JSONException e) {
|
||||
LOG.warn("JSON error", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void addWidgets(ArrayList<HybridHRWatchfaceWidget> widgets) {
|
||||
for (HybridHRWatchfaceWidget widget : widgets) {
|
||||
addWidget(widget);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] getWapp(Context context) throws IOException {
|
||||
byte[] backgroundBytes = ImageConverter.encodeToRawImage(ImageConverter.get2BitsRAWImageBytes(background));
|
||||
InputStream backgroundStream = new ByteArrayInputStream(backgroundBytes);
|
||||
LinkedHashMap<String, InputStream> code = new LinkedHashMap<>();
|
||||
try {
|
||||
code.put(watchfaceName, context.getAssets().open("fossil_hr/openSourceWatchface"));
|
||||
code.put("widgetDate", context.getAssets().open("fossil_hr/widgetDate"));
|
||||
code.put("widgetWeather", context.getAssets().open("fossil_hr/widgetWeather"));
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Unable to read asset file", e);
|
||||
}
|
||||
LinkedHashMap<String, InputStream> icons = new LinkedHashMap<>();
|
||||
try {
|
||||
icons.put("background.raw", backgroundStream);
|
||||
icons.put("icWthClearDay", context.getAssets().open("fossil_hr/icWthClearDay"));
|
||||
icons.put("icWthClearNite", context.getAssets().open("fossil_hr/icWthClearNite"));
|
||||
icons.put("icWthCloudy", context.getAssets().open("fossil_hr/icWthCloudy"));
|
||||
icons.put("icWthPartCloudyDay", context.getAssets().open("fossil_hr/icWthPartCloudyDay"));
|
||||
icons.put("icWthPartCloudyNite", context.getAssets().open("fossil_hr/icWthPartCloudyNite"));
|
||||
icons.put("icWthRainy", context.getAssets().open("fossil_hr/icWthRainy"));
|
||||
icons.put("icWthSnowy", context.getAssets().open("fossil_hr/icWthSnowy"));
|
||||
icons.put("icWthStormy", context.getAssets().open("fossil_hr/icWthStormy"));
|
||||
icons.put("icWthWindy", context.getAssets().open("fossil_hr/icWthWindy"));
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Unable to read asset file", e);
|
||||
}
|
||||
LinkedHashMap<String, String> layout = new LinkedHashMap<>();
|
||||
try {
|
||||
layout.put("complication_layout", getComplicationLayout());
|
||||
} catch (JSONException e) {
|
||||
LOG.warn("Could not generate complication_layout", e);
|
||||
}
|
||||
try {
|
||||
layout.put("image_layout", getImageLayout());
|
||||
} catch (JSONException e) {
|
||||
LOG.warn("Could not generate image_layout", e);
|
||||
}
|
||||
LinkedHashMap<String, String> displayName = new LinkedHashMap<>();
|
||||
displayName.put("display_name", watchfaceName);
|
||||
displayName.put("theme_class", "complications");
|
||||
LinkedHashMap<String, String> config = new LinkedHashMap<>();
|
||||
try {
|
||||
config.put("customWatchFace", getConfiguration());
|
||||
} catch (JSONException e) {
|
||||
LOG.warn("Could not generate configuration", e);
|
||||
}
|
||||
FossilAppWriter appWriter = new FossilAppWriter(context, "1.2.0.0", code, icons, layout, displayName, config);
|
||||
return appWriter.getWapp();
|
||||
}
|
||||
|
||||
private String getComplicationLayout() throws JSONException {
|
||||
JSONArray complicationLayout = new JSONArray();
|
||||
|
||||
JSONObject complicationBackground = new JSONObject();
|
||||
complicationBackground.put("id", 0);
|
||||
complicationBackground.put("type", "complication_background");
|
||||
complicationBackground.put("background", "#background");
|
||||
complicationBackground.put("visible", true);
|
||||
complicationBackground.put("inversion", false);
|
||||
JSONObject goalRing = new JSONObject();
|
||||
goalRing.put("is_enable", "#goal_ring");
|
||||
goalRing.put("end_angle", "#fi");
|
||||
goalRing.put("is_invert", "#$e");
|
||||
complicationBackground.put("goal_ring", goalRing);
|
||||
JSONObject dimension = new JSONObject();
|
||||
dimension.put("type", "rigid");
|
||||
dimension.put("width", "#size.w");
|
||||
dimension.put("height", "#size.h");
|
||||
complicationBackground.put("dimension", dimension);
|
||||
JSONObject placement = new JSONObject();
|
||||
placement.put("type", "absolute");
|
||||
placement.put("left", "#pos.Ue");
|
||||
placement.put("top", "#pos.Qe");
|
||||
complicationBackground.put("placement", placement);
|
||||
complicationLayout.put(complicationBackground);
|
||||
|
||||
JSONObject complicationContent = new JSONObject();
|
||||
complicationContent.put("id", 1);
|
||||
complicationContent.put("parent_id", 0);
|
||||
complicationContent.put("type", "complication_content");
|
||||
complicationContent.put("icon", "#icon");
|
||||
complicationContent.put("text_high", "#dt");
|
||||
complicationContent.put("text_low", "#ci");
|
||||
complicationContent.put("visible", true);
|
||||
complicationContent.put("inversion", "#$e");
|
||||
dimension = new JSONObject();
|
||||
dimension.put("type", "rigid");
|
||||
dimension.put("width", 76);
|
||||
dimension.put("height", 76);
|
||||
complicationContent.put("dimension", dimension);
|
||||
placement = new JSONObject();
|
||||
placement.put("type", "relative");
|
||||
complicationContent.put("placement", placement);
|
||||
complicationLayout.put(complicationContent);
|
||||
|
||||
return complicationLayout.toString();
|
||||
}
|
||||
|
||||
private String getImageLayout() throws JSONException {
|
||||
JSONArray imageLayout = new JSONArray();
|
||||
|
||||
JSONObject container = new JSONObject();
|
||||
container.put("id", 0);
|
||||
container.put("type", "container");
|
||||
container.put("direction", 1);
|
||||
container.put("main_alignment", 1);
|
||||
container.put("cross_alignment", 1);
|
||||
container.put("visible", true);
|
||||
container.put("inversion", false);
|
||||
JSONObject dimension = new JSONObject();
|
||||
dimension.put("type", "rigid");
|
||||
dimension.put("width", 240);
|
||||
dimension.put("height", 240);
|
||||
container.put("dimension", dimension);
|
||||
JSONObject placement = new JSONObject();
|
||||
placement.put("type", "absolute");
|
||||
placement.put("left", 0);
|
||||
placement.put("top", 0);
|
||||
container.put("placement", placement);
|
||||
imageLayout.put(container);
|
||||
|
||||
JSONObject image = new JSONObject();
|
||||
image.put("id", 1);
|
||||
image.put("parent_id", 0);
|
||||
image.put("type", "image");
|
||||
image.put("image_name", "#name");
|
||||
image.put("draw_mode", 1);
|
||||
image.put("visible", true);
|
||||
image.put("inversion", false);
|
||||
placement = new JSONObject();
|
||||
placement.put("type", "absolute");
|
||||
placement.put("left", "#pos.Ue");
|
||||
placement.put("top", "#pos.Qe");
|
||||
image.put("placement", placement);
|
||||
dimension = new JSONObject();
|
||||
dimension.put("width", "#size.w");
|
||||
dimension.put("height", "#size.h");
|
||||
image.put("dimension", dimension);
|
||||
imageLayout.put(image);
|
||||
|
||||
return imageLayout.toString();
|
||||
}
|
||||
|
||||
private String getConfiguration() throws JSONException {
|
||||
JSONObject configuration = new JSONObject();
|
||||
JSONArray layout = new JSONArray();
|
||||
|
||||
JSONObject background = new JSONObject();
|
||||
background.put("type", "image");
|
||||
background.put("name", "background.raw");
|
||||
JSONObject size = new JSONObject();
|
||||
size.put("w", 240);
|
||||
size.put("h", 240);
|
||||
background.put("size", size);
|
||||
JSONObject pos = new JSONObject();
|
||||
pos.put("x", 120);
|
||||
pos.put("y", 120);
|
||||
background.put("pos", pos);
|
||||
layout.put(background);
|
||||
|
||||
for (JSONObject widget : widgets) {
|
||||
layout.put(widget);
|
||||
}
|
||||
|
||||
configuration.put("layout", layout);
|
||||
return configuration.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
/* Copyright (C) 2021 Arjan Schrijver
|
||||
|
||||
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.devices.qhybrid;
|
||||
|
||||
public class HybridHRWatchfaceWidget {
|
||||
private String widgetType;
|
||||
private int posX;
|
||||
private int posY;
|
||||
|
||||
public HybridHRWatchfaceWidget(String widgetType, int posX, int posY) {
|
||||
this.widgetType = widgetType;
|
||||
this.posX = posX;
|
||||
this.posY = posY;
|
||||
}
|
||||
|
||||
public String getWidgetType() {
|
||||
return widgetType;
|
||||
}
|
||||
|
||||
public int getPosX() {
|
||||
return posX;
|
||||
}
|
||||
|
||||
public int getPosY() {
|
||||
return posY;
|
||||
}
|
||||
}
|
@ -176,6 +176,11 @@ public class QHybridCoordinator extends AbstractDeviceCoordinator {
|
||||
return isHybridHR() ? AppManagerActivity.class : ConfigActivity.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Activity> getWatchfaceDesignerActivity() {
|
||||
return isHybridHR() ? HybridHRWatchfaceDesignerActivity.class : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the directory containing the watch app cache.
|
||||
* @throws IOException when the external files directory cannot be accessed
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2019-2021 Andreas Shimokawa, Carsten Pfeiffer, Daniel Dakhno
|
||||
/* Copyright (C) 2019-2021 Andreas Shimokawa, Carsten Pfeiffer, Daniel Dakhno, Arjan Schrijver
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
@ -153,7 +153,6 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
|
||||
private CallSpec currentCallSpec = null;
|
||||
private MusicSpec currentSpec = null;
|
||||
|
||||
int imageNameIndex = 0;
|
||||
private byte jsonIndex = 0;
|
||||
|
||||
private AssetImage backGroundImage = null;
|
||||
@ -1243,7 +1242,6 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
|
||||
});*/
|
||||
}
|
||||
|
||||
|
||||
public byte[] getSecretKey() throws IllegalAccessException {
|
||||
byte[] authKeyBytes = new byte[16];
|
||||
|
||||
|
@ -17,78 +17,24 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.image;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.ColorSpace;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.image.ImageConverter.encodeToRLEImage;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.image.ImageConverter.encodeToRawImage;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.image.ImageConverter.get2BitsRAWImageBytes;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.image.ImageConverter.get2BitsRLEImageBytes;
|
||||
|
||||
public class AssetImageFactory {
|
||||
public static AssetImage createAssetImage(byte[] fileData, int angle, int distance, int indexZ){
|
||||
return new AssetImage(fileData, angle, distance, indexZ);
|
||||
}
|
||||
|
||||
// method created for creating empty files, which turned out not to work anyway
|
||||
private static AssetImage createAssetImage(byte[] fileData, String fileName, int angle, int distance, int indexZ){
|
||||
return new AssetImage(fileData, fileName, angle, distance, indexZ);
|
||||
}
|
||||
|
||||
public static AssetImage createAssetImage(Bitmap fileData, boolean RLEencode, int angle, int distance, int indexZ) throws IOException {
|
||||
if(RLEencode == (distance == 0)) throw new RuntimeException("when RLEencoding distance must be 0, image must be at center of screen");
|
||||
if(RLEencode){
|
||||
int height = fileData.getHeight();
|
||||
int width = fileData.getWidth();
|
||||
|
||||
// if(fileData.getConfig() != Bitmap.Config.ALPHA_8) throw new RuntimeException("Bitmap is not ALPHA_8");
|
||||
|
||||
int[] pixels = new int[height * width];
|
||||
|
||||
fileData.getPixels(pixels, 0, width, 0, 0, width, height);
|
||||
|
||||
byte[] pixelBytes = new byte[width * height];
|
||||
|
||||
for(int i = 0; i < pixels.length; i++){
|
||||
int monochrome = convertToMonochrome(pixels[i]);
|
||||
monochrome >>= 6;
|
||||
|
||||
int alpha = Color.alpha(pixels[i]);
|
||||
monochrome |= (~((alpha & 0xFF) >> 4) & 0b00001100);
|
||||
|
||||
pixelBytes[i] = (byte) monochrome;
|
||||
}
|
||||
return new AssetImage(ImageConverter.encodeToRLEImage(pixelBytes, height, width), angle, distance, indexZ);
|
||||
return new AssetImage(encodeToRLEImage(get2BitsRLEImageBytes(fileData), fileData.getHeight(), fileData.getWidth()), angle, distance, indexZ);
|
||||
}else{
|
||||
// applies only to big background
|
||||
int width = 240;
|
||||
int height = 240;
|
||||
|
||||
byte[] pixelBytes = new byte[width * height];
|
||||
|
||||
float jumpX = fileData.getWidth() / (float) width;
|
||||
float jumpY = fileData.getHeight() / (float) height;
|
||||
for(int y = 0; y < height; y++){
|
||||
for(int x = 0; x < width; x++){
|
||||
int pixel = fileData.getPixel((int)(x * jumpX), (int)(y * jumpY));
|
||||
|
||||
int monochrome = convertToMonochrome(pixel);
|
||||
|
||||
pixelBytes[pixelBytes.length - 1 - (y * width + x)] = (byte) monochrome;
|
||||
}
|
||||
}
|
||||
|
||||
return new AssetImage(ImageConverter.encodeToRawImage(pixelBytes), angle, distance, indexZ);
|
||||
return new AssetImage(encodeToRawImage(get2BitsRAWImageBytes(fileData)), angle, distance, indexZ);
|
||||
}
|
||||
}
|
||||
|
||||
private static @ColorInt int convertToMonochrome(@ColorInt int color){
|
||||
int sum = Color.red(color) + Color.green(color) + Color.blue(color);
|
||||
|
||||
sum /= 3;
|
||||
|
||||
return sum;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2020-2021 Daniel Dakhno
|
||||
/* Copyright (C) 2020-2021 Daniel Dakhno, Arjan Schrijver
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
@ -17,6 +17,9 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.image;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
@ -24,8 +27,35 @@ import java.io.IOException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.encoder.RLEEncoder;
|
||||
|
||||
public class ImageConverter {
|
||||
public static void encodeToTwoBitImage(byte monochromeImage){
|
||||
public static byte[] get2BitsRLEImageBytes(Bitmap bitmap) {
|
||||
int[] pixels = new int[bitmap.getWidth() * bitmap.getHeight()];
|
||||
bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
|
||||
byte[] b_pixels = new byte[pixels.length];
|
||||
for (int i = 0; i < pixels.length; i++) {
|
||||
int monochrome = convertToMonochrome(pixels[i]);
|
||||
monochrome >>= 6;
|
||||
|
||||
int alpha = Color.alpha(pixels[i]);
|
||||
monochrome |= (~((alpha & 0xFF) >> 4) & 0b00001100);
|
||||
|
||||
b_pixels[i] = (byte) monochrome;
|
||||
}
|
||||
return b_pixels;
|
||||
}
|
||||
|
||||
public static byte[] get2BitsRAWImageBytes(Bitmap bitmap) {
|
||||
int width = bitmap.getWidth();
|
||||
int height = bitmap.getHeight();
|
||||
byte[] pixelBytes = new byte[width * height];
|
||||
|
||||
for(int y = 0; y < height; y++){
|
||||
for(int x = 0; x < width; x++){
|
||||
int pixel = bitmap.getPixel(x, y);
|
||||
int monochrome = convertToMonochrome(pixel);
|
||||
pixelBytes[pixelBytes.length - 1 - (y * width + x)] = (byte) monochrome;
|
||||
}
|
||||
}
|
||||
return pixelBytes;
|
||||
}
|
||||
|
||||
public static byte[] encodeToRLEImage(byte[] monochromeImage, int height, int width) throws IOException {
|
||||
@ -57,4 +87,10 @@ public class ImageConverter {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static @ColorInt int convertToMonochrome(@ColorInt int color){
|
||||
int sum = Color.red(color) + Color.green(color) + Color.blue(color);
|
||||
sum /= 3;
|
||||
return sum;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2019-2021 Daniel Dakhno
|
||||
/* Copyright (C) 2019-2021 Daniel Dakhno, Arjan Schrijver
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
@ -17,16 +17,13 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.notification;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorMatrix;
|
||||
import android.graphics.ColorMatrixColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.AssetFile;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.image.ImageConverter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.BitmapUtil;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.encoder.RLEEncoder.RLEEncode;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.util.BitmapUtil.convertDrawableToBitmap;
|
||||
|
||||
public class NotificationImage extends AssetFile {
|
||||
public static final int MAX_ICON_WIDTH = 24;
|
||||
@ -41,7 +38,7 @@ public class NotificationImage extends AssetFile {
|
||||
}
|
||||
|
||||
public NotificationImage(String fileName, Bitmap iconBitmap) {
|
||||
super(fileName, RLEEncode(get2BitsPixelsFromBitmap(convertIcon(iconBitmap))));
|
||||
super(fileName, RLEEncode(ImageConverter.get2BitsRLEImageBytes(BitmapUtil.scaleWithMax(iconBitmap, MAX_ICON_WIDTH, MAX_ICON_HEIGHT))));
|
||||
this.width = Math.min(iconBitmap.getWidth(), MAX_ICON_WIDTH);
|
||||
this.height = Math.min(iconBitmap.getHeight(), MAX_ICON_HEIGHT);
|
||||
}
|
||||
@ -51,41 +48,8 @@ public class NotificationImage extends AssetFile {
|
||||
public int getWidth() { return width; }
|
||||
public int getHeight() { return height; }
|
||||
|
||||
private static Bitmap convertIcon(Bitmap bitmap) {
|
||||
// Scale image only if necessary
|
||||
if ((bitmap.getWidth() > MAX_ICON_WIDTH) || (bitmap.getHeight() > MAX_ICON_HEIGHT)) {
|
||||
bitmap = Bitmap.createScaledBitmap(bitmap, MAX_ICON_WIDTH, MAX_ICON_HEIGHT, true);
|
||||
}
|
||||
// Convert to grayscale
|
||||
Canvas c = new Canvas(bitmap);
|
||||
Paint paint = new Paint();
|
||||
ColorMatrix cm = new ColorMatrix();
|
||||
cm.setSaturation(0);
|
||||
ColorMatrixColorFilter f = new ColorMatrixColorFilter(cm);
|
||||
paint.setColorFilter(f);
|
||||
c.drawBitmap(bitmap, 0, 0, paint);
|
||||
// Increase brightness
|
||||
// bitmap = changeBitmapContrastBrightness(bitmap, 1, -50);
|
||||
// Return result
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
public static byte[] get2BitsPixelsFromBitmap(Bitmap bitmap) {
|
||||
// Downsample to 2 bits image
|
||||
int[] pixels = new int[bitmap.getWidth() * bitmap.getHeight()];
|
||||
bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
|
||||
byte[] b_pixels = new byte[pixels.length];
|
||||
for (int i = 0; i < pixels.length; i++) {
|
||||
b_pixels[i] = (byte) (pixels[i] >> 6 & 0x03);
|
||||
}
|
||||
return b_pixels;
|
||||
}
|
||||
|
||||
public static byte[] getEncodedIconFromDrawable(Drawable drawable) {
|
||||
Bitmap icIncomingCallBitmap = convertDrawableToBitmap(drawable);
|
||||
if ((icIncomingCallBitmap.getWidth() > MAX_ICON_WIDTH) || (icIncomingCallBitmap.getHeight() > MAX_ICON_HEIGHT)) {
|
||||
icIncomingCallBitmap = Bitmap.createScaledBitmap(icIncomingCallBitmap, MAX_ICON_WIDTH, MAX_ICON_HEIGHT, true);
|
||||
}
|
||||
return RLEEncode(NotificationImage.get2BitsPixelsFromBitmap(icIncomingCallBitmap));
|
||||
Bitmap iconBitmap = BitmapUtil.scaleWithMax(BitmapUtil.convertDrawableToBitmap(drawable), MAX_ICON_WIDTH, MAX_ICON_HEIGHT);
|
||||
return RLEEncode(ImageConverter.get2BitsRLEImageBytes(iconBitmap));
|
||||
}
|
||||
}
|
||||
|
@ -22,10 +22,29 @@ import android.graphics.Canvas;
|
||||
import android.graphics.ColorMatrix;
|
||||
import android.graphics.ColorMatrixColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffXfermode;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
public class BitmapUtil {
|
||||
/**
|
||||
* Downscale a bitmap to a maximum resolution. Doesn't scale if the bitmap is already smaller than the max resolution.
|
||||
*
|
||||
* @param bitmap
|
||||
* @param maxWidth
|
||||
* @param maxHeight
|
||||
* @return
|
||||
*/
|
||||
public static Bitmap scaleWithMax(Bitmap bitmap, int maxWidth, int maxHeight) {
|
||||
// Scale image only if necessary
|
||||
if ((bitmap.getWidth() > maxWidth) || (bitmap.getHeight() > maxHeight)) {
|
||||
bitmap = Bitmap.createScaledBitmap(bitmap, maxWidth, maxHeight, true);
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a Bitmap from any given Drawable.
|
||||
@ -50,6 +69,23 @@ public class BitmapUtil {
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the provided Bitmap to grayscale.
|
||||
*
|
||||
* @param bitmap
|
||||
* @return
|
||||
*/
|
||||
public static Bitmap convertToGrayscale(Bitmap bitmap) {
|
||||
Canvas c = new Canvas(bitmap);
|
||||
Paint paint = new Paint();
|
||||
ColorMatrix cm = new ColorMatrix();
|
||||
cm.setSaturation(0);
|
||||
ColorMatrixColorFilter f = new ColorMatrixColorFilter(cm);
|
||||
paint.setColorFilter(f);
|
||||
c.drawBitmap(bitmap, 0, 0, paint);
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the contrast and brightness on a Bitmap
|
||||
*
|
||||
@ -81,4 +117,37 @@ public class BitmapUtil {
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Crops a circular image from the center of the provided Bitmap.
|
||||
* From: https://www.tutorialspoint.com/android-how-to-crop-circular-area-from-bitmap
|
||||
* @param srcBitmap
|
||||
* @return
|
||||
*/
|
||||
public static Bitmap getCircularBitmap(Bitmap srcBitmap) {
|
||||
// Calculate the circular bitmap width with border
|
||||
int squareBitmapWidth = Math.min(srcBitmap.getWidth(), srcBitmap.getHeight());
|
||||
// Initialize a new instance of Bitmap
|
||||
Bitmap dstBitmap = Bitmap.createBitmap (
|
||||
squareBitmapWidth, // Width
|
||||
squareBitmapWidth, // Height
|
||||
Bitmap.Config.ARGB_8888 // Config
|
||||
);
|
||||
Canvas canvas = new Canvas(dstBitmap);
|
||||
// Initialize a new Paint instance
|
||||
Paint paint = new Paint();
|
||||
paint.setAntiAlias(true);
|
||||
Rect rect = new Rect(0, 0, squareBitmapWidth, squareBitmapWidth);
|
||||
RectF rectF = new RectF(rect);
|
||||
canvas.drawOval(rectF, paint);
|
||||
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
|
||||
// Calculate the left and top of copied bitmap
|
||||
float left = (squareBitmapWidth-srcBitmap.getWidth())/2;
|
||||
float top = (squareBitmapWidth-srcBitmap.getHeight())/2;
|
||||
canvas.drawBitmap(srcBitmap, left, top, paint);
|
||||
// Free the native object associated with this bitmap.
|
||||
srcBitmap.recycle();
|
||||
// Return the circular bitmap
|
||||
return dstBitmap;
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,20 @@
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_gravity="bottom|end"
|
||||
app:srcCompat="@drawable/ic_add"
|
||||
app:srcCompat="@android:drawable/stat_sys_upload"
|
||||
android:layout_margin="16dp" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fab_new"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="90dp"
|
||||
app:srcCompat="@drawable/ic_add"
|
||||
android:visibility="invisible"
|
||||
/>
|
||||
|
||||
</android.widget.RelativeLayout>
|
||||
|
@ -0,0 +1,79 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context=".devices.qhybrid.HybridHRWatchfaceDesignerActivity">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/watchface_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginStart="10dp"
|
||||
android:text="NewWatchface"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_edit_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:text="Edit name" />
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/watchface_preview_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginBottom="20dp">
|
||||
<ImageView
|
||||
android:id="@+id/hybridhr_background_image"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true" />
|
||||
</RelativeLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_set_background"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Select background image" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_add_widget"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Add widget" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_watchface_settings"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:enabled="false"
|
||||
android:text="Watchface settings" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_preview_watchface"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Preview on watch" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_save_watchface"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Save and send to watch" />
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
Loading…
Reference in New Issue
Block a user