rodzic
943ac36c6e
commit
ed7d1f897b
|
@ -21,6 +21,7 @@ import java.io.OutputStreamWriter;
|
|||
import java.io.PrintWriter;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class BlueAdapter {
|
||||
private final Activity rootActivity;
|
||||
|
@ -159,11 +160,14 @@ public class BlueAdapter {
|
|||
});
|
||||
}
|
||||
|
||||
class BlockedReaderThread implements Runnable {
|
||||
public class BlockedReaderThread implements Runnable {
|
||||
private String lastLine;
|
||||
private boolean new_line = false;
|
||||
|
||||
private Object lock = new Object();
|
||||
@Override
|
||||
public void run() {
|
||||
for (;;) {
|
||||
while (!Thread.interrupted()) { // kill thread when interruptd
|
||||
System.out.println("BTHREAD: loop ");
|
||||
String line = readLine(); // this fails in all cases (device offline, closed transmission error)
|
||||
if (line == null) {
|
||||
|
@ -173,16 +177,44 @@ public class BlueAdapter {
|
|||
} else {
|
||||
System.out.println("BTHREAD: received " + line);
|
||||
}
|
||||
lastLine = line;
|
||||
|
||||
synchronized (lock) {
|
||||
lastLine = line;
|
||||
new_line = true;
|
||||
}
|
||||
|
||||
try { Thread.sleep(200); } catch (InterruptedException ignored) {}
|
||||
}
|
||||
System.out.println("BTHREAD: exit");
|
||||
}
|
||||
|
||||
public String getLine() {
|
||||
return lastLine;
|
||||
String line;
|
||||
synchronized (lock) {
|
||||
if (new_line) {
|
||||
new_line = false;
|
||||
line = lastLine;
|
||||
} else {
|
||||
line = null;
|
||||
}
|
||||
}
|
||||
return line;
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
try {
|
||||
reader.close();
|
||||
writer.close();
|
||||
} catch (IOException ignored) {}
|
||||
|
||||
try {
|
||||
bluetoothSocket.close();
|
||||
} catch (IOException ignored) {}
|
||||
|
||||
bluetoothSocket = null;
|
||||
reader = null;
|
||||
writer = null;
|
||||
}
|
||||
|
||||
private BlockedReaderThread thread = null;
|
||||
|
|
|
@ -41,6 +41,8 @@ public class DataCollector implements Runnable {
|
|||
private SlideshowFragment compassUpdater = null;
|
||||
private boolean stop = false;
|
||||
|
||||
private BlueAdapter ba;
|
||||
|
||||
public boolean showSondeSet = false;
|
||||
|
||||
public DataCollector(Activity rootActivity) {
|
||||
|
@ -52,6 +54,9 @@ public class DataCollector implements Runnable {
|
|||
locationProvider.startLocationProvider(null);
|
||||
|
||||
orientationProvider = new Orientation(rootActivity);
|
||||
|
||||
ba = new BlueAdapter(rootActivity);
|
||||
ba.setDeviceAddress("aaa (98:F4:AB:6D:2B:5E)");
|
||||
}
|
||||
|
||||
public void setMapUpdater(MapUpdater mapUpdater) {
|
||||
|
@ -115,7 +120,9 @@ public class DataCollector implements Runnable {
|
|||
rs_col.setSondeName(sharedPref.getString("rsid",""));
|
||||
sh_col.setSondeName(sharedPref.getString("shid",""));
|
||||
|
||||
lc_col = new LocalServerCollector(sharedPref.getString("lsip",""));
|
||||
lc_col = new LocalServerCollector();
|
||||
|
||||
lc_col.setPipeSource(sharedPref.getString("lsip",""));
|
||||
|
||||
rs_col_thread = new Thread(rs_col, "rscol");
|
||||
sh_col_thread = new Thread(sh_col, "shcol");
|
||||
|
@ -142,7 +149,6 @@ public class DataCollector implements Runnable {
|
|||
Sonde lc_last_sonde = lc_col.getLastSonde();
|
||||
Sonde sh_last_sonde = sh_col.getLastSonde();
|
||||
|
||||
|
||||
if (lc_last_sonde != null && (rs_last_sonde == null || rs_last_sonde.time <= lc_last_sonde.time))
|
||||
updatePosition(lc_last_sonde, "LOCAL");
|
||||
else if (rs_last_sonde != null && (sh_last_sonde == null || sh_last_sonde.time <= rs_last_sonde.time))
|
||||
|
@ -244,12 +250,8 @@ public class DataCollector implements Runnable {
|
|||
|
||||
void updateStatus() {
|
||||
long time = new Date().getTime();
|
||||
int lc = Color.RED;
|
||||
if (time - lc_col.last_success < 10000) {
|
||||
lc = Color.YELLOW;
|
||||
if (time - lc_col.last_decoded < 20000)
|
||||
lc = Color.GREEN;
|
||||
}
|
||||
int lc = lc_col.getStatus() == LocalServerCollector.Status.RED ? Color.RED :
|
||||
(lc_col.getStatus() == LocalServerCollector.Status.YELLOW ? Color.YELLOW : Color.GREEN);
|
||||
|
||||
int scol = (time - sh_col.last_decoded < 60000) ? Color.GREEN : Color.RED;
|
||||
int rcol = (time - rs_col.last_decoded < 60000) ? Color.GREEN : Color.RED;
|
||||
|
|
|
@ -14,133 +14,130 @@ import java.util.Date;
|
|||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
|
||||
import eu.piotro.sondechaser.data.local.LocalServerDownloader;
|
||||
import eu.piotro.sondechaser.data.local.MySondyDownloader;
|
||||
import eu.piotro.sondechaser.data.local.PipeServerDownloader;
|
||||
|
||||
public class LocalServerCollector implements Runnable {
|
||||
private final String BASE_URL;
|
||||
|
||||
private Sonde lastSonde;
|
||||
private Sonde prevSonde;
|
||||
|
||||
private ArrayList<GeoPoint> track;
|
||||
private final Object dataLock = new Object();
|
||||
private ArrayList<GeoPoint> prediction;
|
||||
private Point pred_point;
|
||||
public long last_success;
|
||||
public long last_decoded;
|
||||
private Status status;
|
||||
|
||||
private long last_decoded;
|
||||
|
||||
private int terrain_alt = 0;
|
||||
|
||||
private volatile boolean stop = false;
|
||||
|
||||
public LocalServerCollector(String ip) {
|
||||
BASE_URL = "http://" + ip + "/";
|
||||
public LocalServerCollector() {}
|
||||
|
||||
private enum Mode {
|
||||
NONE,
|
||||
PIPE,
|
||||
MYSONDY,
|
||||
}
|
||||
|
||||
public enum Status {
|
||||
RED,
|
||||
YELLOW,
|
||||
GREEN,
|
||||
}
|
||||
|
||||
private Mode source;
|
||||
|
||||
private PipeServerDownloader pipeDownloader;
|
||||
private MySondyDownloader mySondyDownloader;
|
||||
|
||||
private void disable() {
|
||||
// pipe downloader does not need disabling
|
||||
mySondyDownloader.disable();
|
||||
source = Mode.NONE;
|
||||
}
|
||||
public void setPipeSource(String ip) {
|
||||
disable();
|
||||
pipeDownloader = new PipeServerDownloader(ip);
|
||||
source = Mode.PIPE;
|
||||
}
|
||||
|
||||
public void setMySondySource(BlueAdapter blueAdapter) {
|
||||
disable();
|
||||
mySondyDownloader = new MySondyDownloader(blueAdapter);
|
||||
mySondyDownloader.enable();
|
||||
source = Mode.MYSONDY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
lastSonde = null;
|
||||
track = new ArrayList<>();
|
||||
pred_point = null;
|
||||
prediction = new ArrayList<>();
|
||||
last_success = 0;
|
||||
last_decoded = 0;
|
||||
status = Status.RED;
|
||||
|
||||
while (!stop) {
|
||||
download();
|
||||
getData();
|
||||
generatePrediction();
|
||||
|
||||
try {
|
||||
Thread.sleep(2000);
|
||||
} catch (InterruptedException ignored) {}
|
||||
boolean ignored = Thread.interrupted();
|
||||
}
|
||||
disable();
|
||||
}
|
||||
|
||||
private void downloadData(URL url, SondeParser parser) {
|
||||
try {
|
||||
System.err.println(url);
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
try {
|
||||
System.err.println(conn.getResponseCode());
|
||||
if (conn.getResponseCode() == 200) {
|
||||
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
||||
StringBuilder resp = new StringBuilder();
|
||||
for (String line; (line = br.readLine()) != null; resp.append(line));
|
||||
parser.parse(resp.toString());
|
||||
last_success = new Date().getTime();
|
||||
}
|
||||
private void getData() {
|
||||
if (source == Mode.NONE) {
|
||||
status = Status.RED;
|
||||
return;
|
||||
}
|
||||
LocalServerDownloader downloader = (source == Mode.PIPE ? pipeDownloader : mySondyDownloader);
|
||||
downloader.download();
|
||||
|
||||
} finally {
|
||||
conn.disconnect();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
prevSonde = lastSonde;
|
||||
synchronized (dataLock) {
|
||||
lastSonde = downloader.getLastSonde();
|
||||
last_decoded = downloader.getLastDecoded();
|
||||
status = downloader.getStatus();
|
||||
}
|
||||
}
|
||||
|
||||
private class Parser implements SondeParser {
|
||||
public void parse(String data) {
|
||||
try {
|
||||
JSONObject json = new JSONObject(data);
|
||||
if (json.length() == 0 || !json.getBoolean("valid")) {
|
||||
return;
|
||||
}
|
||||
|
||||
Sonde sonde = new Sonde();
|
||||
|
||||
float lat = (float)json.getDouble("lat");
|
||||
float lon = (float)json.getDouble("lon");
|
||||
sonde.loc = new GeoPoint(lat, lon);
|
||||
|
||||
sonde.alt = (int)Math.round(json.getDouble("alt"));
|
||||
|
||||
sonde.time = json.getLong("time")*1000;
|
||||
|
||||
sonde.vspeed = (float)json.getDouble("vs");
|
||||
|
||||
sonde.freq = null;
|
||||
sonde.sid = null;
|
||||
|
||||
System.out.println(sonde.alt);
|
||||
if (lastSonde != null && sonde.time == lastSonde.time)
|
||||
return;
|
||||
|
||||
if(sonde.time > new Date().getTime() && sonde.time - new Date().getTime() < 600_000) {
|
||||
// Sonde clocks tend to shift in time
|
||||
sonde.time = new Date().getTime();
|
||||
}
|
||||
|
||||
if (lastSonde != null) {
|
||||
float timedev = (sonde.time - lastSonde.time) / 1000.f;
|
||||
float latdev = (float)(sonde.loc.getLatitude() - lastSonde.loc.getLatitude()) / timedev;
|
||||
float londev = (float)(sonde.loc.getLongitude() - lastSonde.loc.getLongitude()) / timedev;
|
||||
float tgalt = terrain_alt;
|
||||
float nextlat = (float)sonde.loc.getLatitude() + (latdev * ((sonde.alt - tgalt) / (sonde.vspeed * -1)));
|
||||
float nextlon = (float)sonde.loc.getLongitude() + (londev * ((sonde.alt - tgalt) / (sonde.vspeed * -1)));
|
||||
|
||||
synchronized (dataLock) {
|
||||
pred_point = new Point();
|
||||
pred_point.point = new GeoPoint(nextlat, nextlon);
|
||||
pred_point.alt = (int) tgalt;
|
||||
pred_point.time = 0;
|
||||
prediction = new ArrayList<>();
|
||||
prediction.add(sonde.loc);
|
||||
prediction.add(pred_point.point);
|
||||
}
|
||||
}
|
||||
private void generatePrediction() {
|
||||
Sonde sonde = lastSonde;
|
||||
{
|
||||
Sonde lastSonde = prevSonde; // shadow
|
||||
if (sonde.time > new Date().getTime() && sonde.time - new Date().getTime() < 600_000) {
|
||||
// Sonde clocks tend to shift in time
|
||||
sonde.time = new Date().getTime();
|
||||
}
|
||||
if (lastSonde != null) {
|
||||
float timedev = (sonde.time - lastSonde.time) / 1000.f;
|
||||
float latdev = (float) (sonde.loc.getLatitude() - lastSonde.loc.getLatitude()) / timedev;
|
||||
float londev = (float) (sonde.loc.getLongitude() - lastSonde.loc.getLongitude()) / timedev;
|
||||
float tgalt = terrain_alt;
|
||||
float nextlat = (float) sonde.loc.getLatitude() + (latdev * ((sonde.alt - tgalt) / (sonde.vspeed * -1)));
|
||||
float nextlon = (float) sonde.loc.getLongitude() + (londev * ((sonde.alt - tgalt) / (sonde.vspeed * -1)));
|
||||
|
||||
synchronized (dataLock) {
|
||||
System.out.println("localupd");
|
||||
lastSonde = sonde;
|
||||
track.add(sonde.loc);
|
||||
pred_point = new Point();
|
||||
pred_point.point = new GeoPoint(nextlat, nextlon);
|
||||
pred_point.alt = (int) tgalt;
|
||||
pred_point.time = 0;
|
||||
prediction = new ArrayList<>();
|
||||
prediction.add(sonde.loc);
|
||||
prediction.add(pred_point.point);
|
||||
}
|
||||
last_decoded = new Date().getTime();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void download() {
|
||||
try {
|
||||
URL url = new URL(BASE_URL + "get");
|
||||
downloadData(url, new Parser());
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
public Sonde getLastSonde() {
|
||||
synchronized (dataLock) {
|
||||
return lastSonde;
|
||||
|
@ -161,6 +158,14 @@ public class LocalServerCollector implements Runnable {
|
|||
synchronized (dataLock) {return pred_point;}
|
||||
}
|
||||
|
||||
public Status getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public long getLastDecoded() {
|
||||
return last_decoded;
|
||||
}
|
||||
|
||||
public void updateTerrainAlt(int alt) {
|
||||
terrain_alt = alt;
|
||||
}
|
||||
|
|
|
@ -225,7 +225,7 @@ public class RadiosondyCollector implements Runnable {
|
|||
//sonde.vspeed = 0;
|
||||
|
||||
String time_str = curr.getJSONObject("properties").getString("description");
|
||||
System.err.println(time_str.substring(11, 11+19));
|
||||
System.err.println("Radiosondy" + time_str.substring(11, 11+19));
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
sonde.time = sdf.parse(time_str.substring(11, 11+19)).getTime();
|
||||
|
@ -239,7 +239,6 @@ public class RadiosondyCollector implements Runnable {
|
|||
for (int i=path.length()-1; i>=0; i-=10) {
|
||||
JSONArray entry = path.getJSONArray(i);
|
||||
gps.add(new GeoPoint(entry.getDouble(1), entry.getDouble(0)));
|
||||
System.out.println(entry.getDouble(0));
|
||||
}
|
||||
synchronized (dataLock) {
|
||||
track = gps;
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package eu.piotro.sondechaser.data.local;
|
||||
|
||||
import eu.piotro.sondechaser.data.LocalServerCollector;
|
||||
import eu.piotro.sondechaser.data.Sonde;
|
||||
|
||||
public interface LocalServerDownloader {
|
||||
void download();
|
||||
|
||||
Sonde getLastSonde();
|
||||
long getLastDecoded();
|
||||
LocalServerCollector.Status getStatus();
|
||||
|
||||
void enable();
|
||||
void disable();
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package eu.piotro.sondechaser.data.local;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import eu.piotro.sondechaser.data.BlueAdapter;
|
||||
import eu.piotro.sondechaser.data.LocalServerCollector;
|
||||
import eu.piotro.sondechaser.data.Sonde;
|
||||
|
||||
public class MySondyDownloader implements LocalServerDownloader {
|
||||
private final BlueAdapter blueAdapter;
|
||||
|
||||
private BlueAdapter.BlockedReaderThread bt_runnable;
|
||||
private Thread bt_thread;
|
||||
|
||||
private Sonde lastSonde;
|
||||
private long last_decoded;
|
||||
|
||||
private LocalServerCollector.Status mStatus = LocalServerCollector.Status.RED;
|
||||
|
||||
public MySondyDownloader(BlueAdapter blueAdapter) {
|
||||
this.blueAdapter = blueAdapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable() {
|
||||
disable();
|
||||
// Address and mode should be configured already in passed object
|
||||
bt_runnable = blueAdapter.getRunnable();
|
||||
bt_thread = new Thread(bt_runnable);
|
||||
bt_thread.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disable() {
|
||||
if(bt_thread == null)
|
||||
return;
|
||||
bt_thread.interrupt();
|
||||
blueAdapter.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download() {
|
||||
if (!blueAdapter.isConnected())
|
||||
mStatus = LocalServerCollector.Status.RED;
|
||||
else
|
||||
mStatus = LocalServerCollector.Status.YELLOW;
|
||||
|
||||
String line = bt_runnable.getLine();
|
||||
|
||||
if (line == null)
|
||||
return;
|
||||
|
||||
System.out.println("PARSE" + line);
|
||||
|
||||
last_decoded = new Date().getTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Sonde getLastSonde() {
|
||||
return lastSonde;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLastDecoded() {
|
||||
return last_decoded;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalServerCollector.Status getStatus() {
|
||||
return mStatus;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
package eu.piotro.sondechaser.data.local;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.osmdroid.util.GeoPoint;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.Date;
|
||||
|
||||
import eu.piotro.sondechaser.data.LocalServerCollector;
|
||||
import eu.piotro.sondechaser.data.Sonde;
|
||||
import eu.piotro.sondechaser.data.SondeParser;
|
||||
|
||||
public class PipeServerDownloader implements LocalServerDownloader {
|
||||
private final String BASE_URL;
|
||||
|
||||
public PipeServerDownloader(String ip) {
|
||||
BASE_URL = "http://" + ip + "/";
|
||||
}
|
||||
|
||||
private long last_received;
|
||||
private long last_decoded;
|
||||
|
||||
private Sonde lastSonde = null;
|
||||
|
||||
private LocalServerCollector.Status mStatus = LocalServerCollector.Status.RED;
|
||||
|
||||
private void downloadData(URL url, SondeParser parser) {
|
||||
try {
|
||||
System.err.println(url);
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
try {
|
||||
System.err.println(conn.getResponseCode());
|
||||
if (conn.getResponseCode() == 200) {
|
||||
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
||||
StringBuilder resp = new StringBuilder();
|
||||
for (String line; (line = br.readLine()) != null; resp.append(line));
|
||||
parser.parse(resp.toString());
|
||||
last_received = new Date().getTime();
|
||||
}
|
||||
|
||||
} finally {
|
||||
conn.disconnect();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private class Parser implements SondeParser {
|
||||
public void parse(String data) {
|
||||
try {
|
||||
JSONObject json = new JSONObject(data);
|
||||
if (json.length() == 0 || !json.getBoolean("valid")) {
|
||||
return;
|
||||
}
|
||||
|
||||
Sonde sonde = new Sonde();
|
||||
|
||||
float lat = (float)json.getDouble("lat");
|
||||
float lon = (float)json.getDouble("lon");
|
||||
sonde.loc = new GeoPoint(lat, lon);
|
||||
|
||||
sonde.alt = (int)Math.round(json.getDouble("alt"));
|
||||
|
||||
sonde.time = json.getLong("time")*1000;
|
||||
|
||||
sonde.vspeed = (float)json.getDouble("vs");
|
||||
|
||||
sonde.freq = null;
|
||||
sonde.sid = null;
|
||||
|
||||
System.out.println("LocalPipeServer" + sonde.time);
|
||||
|
||||
if (lastSonde != null && sonde.time != lastSonde.time)
|
||||
last_decoded = new Date().getTime();
|
||||
|
||||
lastSonde = sonde;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateStatus() {
|
||||
LocalServerCollector.Status lc = LocalServerCollector.Status.RED;
|
||||
if (new Date().getTime() - last_received < 10000) {
|
||||
lc = LocalServerCollector.Status.YELLOW;
|
||||
if (new Date().getTime() - last_decoded < 20000)
|
||||
lc = LocalServerCollector.Status.GREEN;
|
||||
}
|
||||
mStatus = lc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download() {
|
||||
try {
|
||||
updateStatus();
|
||||
URL url = new URL(BASE_URL + "get");
|
||||
downloadData(url, new Parser());
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Sonde getLastSonde() {
|
||||
return lastSonde;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLastDecoded() {
|
||||
return last_decoded;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalServerCollector.Status getStatus() {
|
||||
return mStatus;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable() {}
|
||||
@Override
|
||||
public void disable() {}
|
||||
}
|
|
@ -20,6 +20,7 @@ import java.time.Duration;
|
|||
|
||||
import eu.piotro.sondechaser.MainActivity;
|
||||
import eu.piotro.sondechaser.R;
|
||||
import eu.piotro.sondechaser.data.BlueAdapter;
|
||||
import eu.piotro.sondechaser.data.RadiosondyCollector;
|
||||
import eu.piotro.sondechaser.data.SondeHubCollector;
|
||||
import eu.piotro.sondechaser.databinding.FragmentGalleryBinding;
|
||||
|
@ -101,6 +102,10 @@ public class GalleryFragment extends Fragment {
|
|||
((TextView)v.findViewById(R.id.tfsh)).setText(entry);
|
||||
return true;
|
||||
});
|
||||
|
||||
// PopupMenu btPopupMenu = new PopupMenu(context, v.findViewById(R.id.set_awake));
|
||||
// new BlueAdapter(getActivity()).fillMenu(getActivity(), btPopupMenu);
|
||||
// btPopupMenu.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -108,4 +108,5 @@
|
|||
android:text="Keep screen awake"
|
||||
app:layout_constraintStart_toStartOf="@+id/tfip"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tfip" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
Ładowanie…
Reference in New Issue