From: Nathaniel Wesley Filardo Date: Tue, 19 Feb 2019 00:55:04 +0000 (+0000) Subject: Dramatic overhaul of Handbook X-Git-Tag: release-1.3~9 X-Git-Url: https://hydra-www.ietfng.org/gitweb/?a=commitdiff_plain;h=0195691afc7cbec3a9c900518722693ebd9bc8ab;p=acmetensortoys-ctfws-android Dramatic overhaul of Handbook Now includes the ability to grab new versiosn as directed by MQTT! While here, ship the latest and greatest by default, tho'. --- diff --git a/mobile/src/main/assets/hand.html b/mobile/src/main/assets/hand.html index 2b150fc..912c73b 100644 --- a/mobile/src/main/assets/hand.html +++ b/mobile/src/main/assets/hand.html @@ -1,8 +1,9 @@ - -Capture the Flag with Stuff + +Capture the Flag with Stuff + - - Capture the Flag with Stuff - +
+ + + + + + + + + + + + + + + +
+

I. Base Rules

-

I.@. Meta

+

I.@. Meta

I.@.1. When rules in this document conflict, the later rule takes precedence.

I.@.1.a. E.g. I.C.4 states that a captor may not capture, while @@ -46,7 +86,7 @@ of Many Places at Once may capture multiple times.

the game. Any such rules or stuff changes must be clearly explained before the start of the game in which those changes are to be introduced.

-

I.A. Teams and Territories

+

I.A. Teams and Territories

I.A.1. The game consists of two teams, red and yellow. Team membership is indicated by colored bands, worn on the upper arm or head, which must be visible at all times.

@@ -66,7 +106,7 @@ part of their body is in the team's territory. If a player is in no team's territory, they are in neutral territory. Magic items have no effect on players who are in neutral territory.

-

I.B. Players

+

I.B. Players

I.B.1. A player is any person playing the game.

I.B.2. A teammate is a member of the same team.

I.B.3. An enemy is a player who is not a teammate.

@@ -76,16 +116,16 @@ interrogation victim, or stunned.

I.B.6. An ethereal player may not use, drop, or pick up magic items, is not affected by magic items, may not capture, and may not be captured. A player must use jazz hands to signal ethereality.

-

I.B.7. A captive is a player who has been captured (see ). +

I.B.7. A captive is a player who has been captured (see I.C). Captives may not move or touch enemy flags and may not use or drop - magic items (except for the Ninja Potion; see ).

+ magic items (except for the Ninja Potion; see III.B).

I.B.8. A captor is a player who has captured another player (see -). Captors must lead their captives to the jail (see ), +I.C). Captors must lead their captives to the jail (see I.C), but are otherwise normal players.

I.B.9. A prisoner is a player who was a captive, but has touched the Glyph of Jail -(see ).

+(see I.C).

I.B.10. An interrogation victim is a prisoner who has been given truth serum -by the jailer (see ). They may not use magic items, but otherwise act +by the jailer (see I.C). They may not use magic items, but otherwise act identically to a prisoner except where specified.

I.B.11. A stunned player must immediately sit on the floor in the territory in which they were stunned until the stun wears off. The time they are stunned is measured from @@ -96,7 +136,7 @@ stunned. Stunned players may not capture.

I.B.12.a. If John Mackey changes a player's major during the game, the player is stunned for one minute.

-

I.C. Capture and Imprisonment

+

I.C. Capture and Imprisonment

I.C.1. If a player touches an enemy when both are in the first player's home territory, they may choose to capture the enemy. At this point, the enemy becomes a captive.

@@ -109,7 +149,7 @@ request of the captor or a teammate of the captor. Concealable items need not be given up unless the enemy asking for them knows that the player has the item.

I.C.4. A captor must lead their captive directly to their team's Glyph -of Jail (see ) at a reasonable speed. They cannot make any more +of Jail (see II.B) at a reasonable speed. They cannot make any more captures until their captive has entered jail by touching the Glyph of Jail or another prisoner in the jail. Once this occurs, the captor is again a normal player, and the captive becomes a @@ -117,7 +157,7 @@ prisoner.

I.C.5. If a captor is stunned, their captive becomes a normal player.

I.C.6. Prisoners may use magic items.

-

I.D. Jail

+

I.D. Jail

I.D.1. Each team has a jail, designated by the Glyph of Jail.

I.D.1.a. Whenever possible, the Glyph of Jail in Doherty will be placed on floor A, near the staircase closest to the main lobby, and the Glyph of Jail in Wean @@ -163,7 +203,7 @@ freed, at jailbreak, if there is no jailer, or if the jailer leaves line of sight of the victim.

I.D.11. There may be at most one truth serum victim in each jail.

-

I.E. Initial Setup and Game Completion

+

I.E. Initial Setup and Game Completion

I.E.1. At the beginning of the game, there is a setup period of fifteen minutes. This time should be used for the placement of flags and glyphs, and distribution of magic items. No player may enter enemy @@ -172,7 +212,7 @@ time.

I.E.2. The game begins at the end of the set up period and ends after one hour, or when there remains only one team with uncaptured flags.

-

I.F. Additional Terms

+

I.F. Additional Terms

I.F.1. Cooldown: This is a property of certain items. An item with cooldown may not be activated again for one minute after its use. This requirement may be suspended by the singing of a number of verses from @@ -203,7 +243,7 @@ activation occurs, and the action occurs upon completion of the phrase.

rules I.C.3 and I.F.2, means that they have not had a chance to dispose of that item since the last time you saw it in their possession.

-

I.G. Flags

+

I.G. Flags

I.G.1. Flags are large pieces of cloth or felt. Each team has three or more flags.

I.G.2. Flags must be placed during the initial setup time, and their placement must be supervised by a judge. They must be placed no higher @@ -229,7 +269,7 @@ must be informed of this fact before the set up of the game in which this takes team the number of points associated with the flag. The flag is then out of play for the remainder of the game.

-

I.H. Judges

+

I.H. Judges

I.H.1. Judges are indicated by blue or silver headbands, and are responsible for keeping the game running smoothly.

I.H.2. The head judge is the final arbiter and scorekeeper, and should remain in the @@ -244,7 +284,7 @@ to both sides. In all cases, the judge's decision is final.

II. Glyphs

-

II.A. General

+

II.A. General

II.A.1. Glyphs are large sheets of paper or poster-board marked with a spell.

II.A.2. Each glyph is owned by a particular team, indicated by the coloring of the glyph.

II.A.3. A glyph must be taped to a wall in the owning team's territory or side @@ -260,7 +300,7 @@ to both sides. In all cases, the judge's decision is final.

II.B. Glyph of Jail

II.B.1. The Glyph of Jail is indicated by the word "Jail".

-

II.B.2. The Glyph of Jail defines the Jail, as described by section .

+

II.B.2. The Glyph of Jail defines the Jail, as described by section I.D.

II.B.3. The Glyph of Jail cannot be dispelled.

II.C. Glyph of Entrancement

@@ -291,13 +331,13 @@ until they are captured or returns to neutral space.

II.F. Glyph of Recharge

II.F.1. The Glyph of Recharge is indicated by the word "Recharge".

-

II.F.2. A player may recharge a white colored magical item (see ) by holding it +

II.F.2. A player may recharge a white colored magical item (see VI.A) by holding it to a Glyph of Recharge for a period of 10 seconds.

II.F.2.a. A player may use any Glyph of Recharge in either territory.

III. Potions

-

III.A. General

+

III.A. General

III.A.1. Potions are small pieces of foam wrapped in colored tape.

III.A.2. Potions are concealable.

@@ -347,7 +387,7 @@ themself and return to a normal state.

IV. Wands

-

IV.A. General

+

IV.A. General

IV.A.1. Wands are eighteen-inch lengths of foam rubber.

IV.A.2. The effect of a wand is activated by whapping a person or glyph and shouting the key word.

@@ -378,7 +418,7 @@ one minute.

V. Belts

-

V.A. General

+

V.A. General

V.A.1. A belt is a long sash of cloth or felt.

V.A.2. To be used, a belt must be tied around a player's waist.

V.A.3. A belt can be worn by at most one player.

@@ -432,7 +472,7 @@ singing along.

VI. White Magic Items

-

VI.A. White Magic Items

+

VI.A. White Magic Items

VI.A.1. A white magic item is white.

VI.A.2. A white magic item must be charged to be used.

VI.A.2.a. All white magic items are charged at the start of the game

@@ -440,9 +480,9 @@ singing along.

VI.A.4. Each white magic item can be used as though it were a magic item of the same type of any other color, and is activated by invoking the keyword and action of that item.

-

VI.A.4.a. A white potion may be activated as any potion defined in , a - white wand as any wand defined in , and a white belt as any belt - defined in .

+

VI.A.4.a. A white potion may be activated as any potion defined in III, a + white wand as any wand defined in IV, and a white belt as any belt + defined in V.

VI.A.5. A white magic item is always white. During its activation, it gains the color of the magic item which was invoked, and follows all rules of that magic item.

@@ -450,18 +490,18 @@ singing along.

player state and location for activating that color and type of magic item

VI.A.5.b. For example, if a white wand is used to invoke a Wand of - Vengeance , you must be in enemy territory, and the white wand is + Vengeance IV.B, you must be in enemy territory, and the white wand is sacrificial.

VI.A.6. After the activation of a white magic item, it becomes discharged and loses the color of the invoked item.

VI.A.6.a. If a white magic item touches the ground, it becomes discharged.

VI.A.7. In order to recharge a white magic item, a player must hold it to - either a Glyph of Recharge (see ) or their own - Glyph of Jail (see ) for a period of ten seconds.

+ either a Glyph of Recharge (see II.F) or their own + Glyph of Jail (see II.B) for a period of ten seconds.

VII. Sportsmanship

-

VII.A. Physicality

+

VII.A. Physicality

VII.A.1. Players may not physically impede or block other players, either with their bodies or with objects.

VII.A.1.a. This includes actions like preventing elevators from @@ -487,19 +527,13 @@ using it.

0. Respect: Above all, don't be a jackass.

-
-
-
-

- Copyright © The Carnegie Mellon KGB.
-

-
- - - - - - - +
+
+
+

+ Copyright © The Carnegie Mellon KGB.
+

+
+
diff --git a/mobile/src/main/assets/handico/belts.png b/mobile/src/main/assets/handico/belts.png deleted file mode 100644 index f6fbed1..0000000 Binary files a/mobile/src/main/assets/handico/belts.png and /dev/null differ diff --git a/mobile/src/main/assets/handico/capture.png b/mobile/src/main/assets/handico/capture.png deleted file mode 100644 index 73fcf10..0000000 Binary files a/mobile/src/main/assets/handico/capture.png and /dev/null differ diff --git a/mobile/src/main/assets/handico/flags.png b/mobile/src/main/assets/handico/flags.png deleted file mode 100644 index ee4b703..0000000 Binary files a/mobile/src/main/assets/handico/flags.png and /dev/null differ diff --git a/mobile/src/main/assets/handico/glyphs.png b/mobile/src/main/assets/handico/glyphs.png deleted file mode 100644 index d793c95..0000000 Binary files a/mobile/src/main/assets/handico/glyphs.png and /dev/null differ diff --git a/mobile/src/main/assets/handico/jail.png b/mobile/src/main/assets/handico/jail.png deleted file mode 100644 index 96f4bb6..0000000 Binary files a/mobile/src/main/assets/handico/jail.png and /dev/null differ diff --git a/mobile/src/main/assets/handico/judges.png b/mobile/src/main/assets/handico/judges.png deleted file mode 100644 index 39c7f67..0000000 Binary files a/mobile/src/main/assets/handico/judges.png and /dev/null differ diff --git a/mobile/src/main/assets/handico/meta.png b/mobile/src/main/assets/handico/meta.png deleted file mode 100644 index e37faec..0000000 Binary files a/mobile/src/main/assets/handico/meta.png and /dev/null differ diff --git a/mobile/src/main/assets/handico/players.png b/mobile/src/main/assets/handico/players.png deleted file mode 100644 index 3fe924e..0000000 Binary files a/mobile/src/main/assets/handico/players.png and /dev/null differ diff --git a/mobile/src/main/assets/handico/potions.png b/mobile/src/main/assets/handico/potions.png deleted file mode 100644 index 310ad59..0000000 Binary files a/mobile/src/main/assets/handico/potions.png and /dev/null differ diff --git a/mobile/src/main/assets/handico/setup.png b/mobile/src/main/assets/handico/setup.png deleted file mode 100644 index f9c946b..0000000 Binary files a/mobile/src/main/assets/handico/setup.png and /dev/null differ diff --git a/mobile/src/main/assets/handico/sportsmanship.png b/mobile/src/main/assets/handico/sportsmanship.png deleted file mode 100644 index 5f292c3..0000000 Binary files a/mobile/src/main/assets/handico/sportsmanship.png and /dev/null differ diff --git a/mobile/src/main/assets/handico/teams_territories.png b/mobile/src/main/assets/handico/teams_territories.png deleted file mode 100644 index 7fef007..0000000 Binary files a/mobile/src/main/assets/handico/teams_territories.png and /dev/null differ diff --git a/mobile/src/main/assets/handico/terms.png b/mobile/src/main/assets/handico/terms.png deleted file mode 100644 index 1ff2729..0000000 Binary files a/mobile/src/main/assets/handico/terms.png and /dev/null differ diff --git a/mobile/src/main/assets/handico/wands.png b/mobile/src/main/assets/handico/wands.png deleted file mode 100644 index f774240..0000000 Binary files a/mobile/src/main/assets/handico/wands.png and /dev/null differ diff --git a/mobile/src/main/assets/handico/white.png b/mobile/src/main/assets/handico/white.png deleted file mode 100644 index 7e53a80..0000000 Binary files a/mobile/src/main/assets/handico/white.png and /dev/null differ diff --git a/mobile/src/main/assets/handord.txt b/mobile/src/main/assets/handord.txt deleted file mode 100644 index d433cff..0000000 --- a/mobile/src/main/assets/handord.txt +++ /dev/null @@ -1,14 +0,0 @@ -meta -teams_territories -players -capture -jail -setup -terms -flags -judges -glyphs -potions -wands -belts -sportsmanship diff --git a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/CheckedAsyncDownloader.java b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/CheckedAsyncDownloader.java new file mode 100644 index 0000000..1b85a3c --- /dev/null +++ b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/CheckedAsyncDownloader.java @@ -0,0 +1,146 @@ +package com.acmetensortoys.ctfwstimer; + +import android.content.Context; +import android.os.AsyncTask; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URL; +import java.net.URLConnection; +import java.nio.file.NoSuchFileException; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class CheckedAsyncDownloader extends AsyncTask { + + public static final long ERR_UNTRIED = -1; /* Not yet tried */ + public static final long ERR_ALREADY = -2; /* Existing file matches checksum */ + public static final long ERR_WRITE = -3; /* Local FS error */ + public static final long ERR_HOSTUNREACH = -4; /* Could not establish connection */ + public static final long ERR_XFER = -5; /* Error during transfer */ + public static final long ERR_CHECKSUM = -6; /* Checksum did not match after xfer */ + + public static class DL { + final URL url; + final byte[] sha256; + final File dest; + long result; + + public DL(URL url, byte[] sha256, File dest) { + this.url = url; + this.sha256 = sha256; + this.dest = dest; + this.result = ERR_UNTRIED; + } + } + + @Override + protected Void doInBackground(DL... dls) { + MessageDigest md; + + try { + md = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException nsae) { + return null; + } + + for (DL dl : dls) { + try { + DigestInputStream is = new DigestInputStream( + new BufferedInputStream(new FileInputStream(dl.dest)), + md); + + byte[] data = new byte[4096]; + + while (is.read(data) != -1) { ; } + + if (java.util.Arrays.equals(is.getMessageDigest().digest(), dl.sha256)) { + dl.result = ERR_ALREADY; + continue; + } + + } catch (FileNotFoundException nsfe) { + /* OK, the file isn't there, just keep going */ + ; + } catch (IOException ioe) { + /* + * Something has gone really wrong. + * Unlink the file and try to fetch it + */ + dl.dest.delete(); + } + + DigestInputStream is; + File oft; + OutputStream os; + long xfer = 0; + + try { + oft = File.createTempFile(dl.dest.getName(), "dl"); + } catch (IOException ioe) { + dl.result = ERR_WRITE; + continue; + } + + try { + os = new FileOutputStream(oft); + } catch (IOException ioe) { + dl.result = ERR_WRITE; + oft.delete(); + continue; + } + + try { + URLConnection urlc = dl.url.openConnection(); + + is = new DigestInputStream( + new BufferedInputStream(urlc.getInputStream()), + md + ); + } catch (IOException ioe) { + dl.result = ERR_HOSTUNREACH; + oft.delete(); + continue; + } + + try { + byte[] data = new byte[4096]; + int count; + + while ((count = is.read(data)) != -1) { + xfer += count; + os.write(data, 0, count); + } + + is.close(); + os.close(); + } catch (IOException ioe) { + dl.result = ERR_XFER; + oft.delete(); + continue; + } + + if (!java.util.Arrays.equals(is.getMessageDigest().digest(), dl.sha256)) { + dl.result = ERR_CHECKSUM; + oft.delete(); + continue; + } + + if (!oft.renameTo(dl.dest)) { + dl.result = ERR_WRITE; + oft.delete(); + continue; + } + + dl.result = xfer; + } + + return null; + } +} \ No newline at end of file diff --git a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/HandbookActivity.java b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/HandbookActivity.java index 7a5f0a4..afeece1 100644 --- a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/HandbookActivity.java +++ b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/HandbookActivity.java @@ -1,11 +1,16 @@ package com.acmetensortoys.ctfwstimer; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; import android.content.res.AssetManager; import android.graphics.BitmapFactory; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.VectorDrawable; import android.net.Uri; +import android.os.IBinder; import android.support.annotation.NonNull; import android.support.v4.content.ContextCompat; import android.support.v7.app.AppCompatActivity; @@ -17,10 +22,15 @@ import android.util.Log; import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; +import android.webkit.WebSettings; import android.webkit.WebView; import android.widget.Button; +import android.widget.Toast; + +import org.eclipse.paho.client.mqttv3.IMqttMessageListener; import java.io.BufferedReader; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -29,104 +39,105 @@ import java.util.List; public class HandbookActivity extends AppCompatActivity { - private class HandbookAdapter extends RecyclerView.Adapter { - - class MyVH extends RecyclerView.ViewHolder { - Button b; - MyVH(Button b) { - super(b); - this.b = b; - } - } + public static final String HAND_FILE_NAME = "handbook.html"; + private static final String TAG = "CtFwSHandbook"; - final String[] fns; - final View.OnClickListener ocl; + private WebView mWV; - HandbookAdapter(String[] fns, View.OnClickListener ocl) { - this.fns = fns; - this.ocl = ocl; + private void display() { + final File dlf = new File(getFilesDir(), HAND_FILE_NAME); + if (dlf.exists()) { + /* render the version we've downloaded */ + mWV.loadUrl(dlf.toURI().toString()); + } else { + /* render the version we were shipped with instead */ + mWV.loadUrl("file:///android_asset/hand.html"); } + } + private final MainService.Observer mSrvObs = new MainService.Observer() { + @Override + public void onMqttServerChanged(MainService.LocalBinder b, String sURL) { + ; + } + @Override + public void onMqttServerEvent(MainService.LocalBinder b, MainService.MqttServerEvent mse) { + ; + } @Override - public MyVH onCreateViewHolder(ViewGroup parent, int tipe) { - Button b = new Button(HandbookActivity.this, null, R.style.Widget_AppCompat_Button_Borderless); - b.setOnClickListener(ocl); - return new MyVH(b); + public void onHandbookFetch(MainService.LocalBinder b) { + display(); + Toast.makeText(HandbookActivity.this, R.string.hand_new, Toast.LENGTH_SHORT).show(); } + }; + private MainService.LocalBinder mSrvBinder; + private final ServiceConnection ctfwssc = new ServiceConnection() { @Override - public void onBindViewHolder(MyVH vh, int pos) { - try { - InputStream is = getAssets().open("handico/" + fns[pos] + ".png"); - Drawable d = BitmapDrawable.createFromStream(is, fns[pos]); - vh.b.setBackground(d); - } catch (IOException ignored) { - String ch = fns[pos]; - int ix = ch.indexOf('-'); - if (ix >= 0) { ch = ch.substring(ix); } - vh.b.setText(ch); - } + public void onServiceConnected(ComponentName name, IBinder service) { + mSrvBinder = (MainService.LocalBinder) service; + mSrvBinder.registerObserver(mSrvObs); + mSrvBinder.connect(false); } @Override - public int getItemCount() { - return fns.length; + public void onServiceDisconnected(ComponentName name) { + mSrvBinder = null; } - } + }; @Override protected void onCreate(Bundle savedInstanceState) { - Log.d("CtFwSHandbook", "onCreate"); + Log.d(TAG, "onCreate"); super.onCreate(savedInstanceState); setContentView(R.layout.activity_handbook); - final WebView wv = findViewById(R.id.hand_wv); - - final String[] sections; - try { - InputStream is = getAssets().open("handord.txt", AssetManager.ACCESS_STREAMING); - BufferedReader br = new BufferedReader(new InputStreamReader(is)); - List lines = new ArrayList<>(); - String line; - while ((line = br.readLine()) != null) { - lines.add(line); - } - sections = lines.toArray(new String[0]); - - final RecyclerView rv = findViewById(R.id.hand_tabs); - rv.setHasFixedSize(true); - - LinearLayoutManager lm = new LinearLayoutManager(this); - rv.setLayoutManager(lm); - - Drawable divdr = ContextCompat.getDrawable(this, R.drawable.hand_tab_div); - if (divdr != null) { - DividerItemDecoration divde = new DividerItemDecoration(rv.getContext(), - lm.getOrientation()); - divde.setDrawable(divdr); - rv.addItemDecoration(divde); - } - - final View.OnClickListener ocl = new View.OnClickListener() { - @Override - public void onClick(View v) { - int ix = rv.getChildAdapterPosition(v); - Log.d("CtFwSHandbook", "onClick avix=" + ix); - if (ix != RecyclerView.NO_POSITION) { - Log.d("CtFwSHandbook" , "onClick avs=" + sections[ix]); - wv.loadUrl("file:///android_asset/hand.html#" + sections[ix]); - } - } - }; - - RecyclerView.Adapter a = new HandbookAdapter(sections, ocl); - rv.setAdapter(a); - } catch (IOException ioe) { - Log.d("CtFwSHandbook", "IOException?", ioe); + mWV = findViewById(R.id.hand_wv); + + WebSettings wvs = mWV.getSettings(); + wvs.setBuiltInZoomControls(true); + wvs.setDisplayZoomControls(false); + display(); + } + + @Override + public void onStart() { + Log.d(TAG, "onStart"); + super.onStart(); + + if (mSrvBinder == null) { + Intent si = new Intent(this, MainService.class); + bindService(si, ctfwssc, Context.BIND_AUTO_CREATE | Context.BIND_ABOVE_CLIENT); } + } + + @Override + public void onResume() { + Log.d(TAG, "onResume"); + super.onResume(); + + if (mSrvBinder != null) { + mSrvBinder.registerObserver(mSrvObs); + } + } + + @Override + protected void onPause() { + Log.d(TAG, "onPause"); + if (mSrvBinder != null) { + mSrvBinder.unregisterObserver(mSrvObs); + } + + super.onPause(); + } + + @Override + protected void onDestroy() { + Log.d(TAG, "onDestroy"); + unbindService(ctfwssc); - wv.loadUrl("file:///android_asset/hand.html"); + super.onDestroy(); } } diff --git a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/HandbookDownloader.java b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/HandbookDownloader.java new file mode 100644 index 0000000..990de78 --- /dev/null +++ b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/HandbookDownloader.java @@ -0,0 +1,189 @@ +package com.acmetensortoys.ctfwstimer; + +import android.content.Context; +import android.util.Log; + +import com.acmetensortoys.ctfwstimer.CheckedAsyncDownloader; + +import org.eclipse.paho.client.mqttv3.IMqttAsyncClient; +import org.eclipse.paho.client.mqttv3.IMqttMessageListener; +import org.eclipse.paho.client.mqttv3.MqttClient; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; + +import java.io.File; +import java.net.URL; +import java.util.NoSuchElementException; +import java.util.Scanner; + +public class HandbookDownloader implements IMqttMessageListener { + + private static String TAG = "HandbookDownloader"; + + private final Context mCtx; + private final Runnable mDLFiniCB; + private IMqttAsyncClient mMqc; + + public HandbookDownloader(Context mCtx, Runnable dlfinicb) { + this.mCtx = mCtx; + this.mDLFiniCB = dlfinicb; + } + + /* non-null if download is in progress; access synchronized on this */ + private CheckedAsyncDownloader downloader; + private CheckedAsyncDownloader.DL download; + private byte[] lastFetchedChecksum; + + /* + * Alright, fetch time. We're going to unsubscribe for the duration of the + * transfer and resubscribe later. This has two effects: it slightly reduces the + * odds that we'll get another message while this transfer is in progress and it + * will cause us to get another message at the end. Hopefully, that later message + * carries the checksum of the file we just downloaded, so we'll skip out without + * redoing the transfer. + * + * Note that the CheckedAsyncDownloader also verifies the checksum of the file + * present on disk, so at startup we might run one AsyncTask and then bail out. + */ + private class Task extends CheckedAsyncDownloader { + final IMqttAsyncClient mqc; + + public Task(IMqttAsyncClient mqc) { + this.mqc = mqc; + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + Log.d(TAG, "Pre-ex"); + synchronized (HandbookDownloader.this) { + try { + unsubscribe(mqc); + } catch (MqttException mqe) { + /* No real harm in the matter */ + ; + } + } + } + + private void fini() { + if (mqc == mMqc) { + try { + subscribe(mqc); + } catch (MqttException mqe) { + /* + * Well this stinks. Presumably it is because something has gone + * wrong somewhere else and we will notice shortly. + */ + ; + } + } + HandbookDownloader.this.downloader = null; + HandbookDownloader.this.download = null; + } + + @Override + protected void onPostExecute(Void aVoid) { + super.onPostExecute(aVoid); + + long result; + synchronized (HandbookDownloader.this) { + DL dl = HandbookDownloader.this.download; + result = dl.result; + if ((result >= 0) || (result == ERR_ALREADY)) { + HandbookDownloader.this.lastFetchedChecksum = dl.sha256; + } + fini(); + } + Log.d(TAG, "Post Ex: " + result); + if (result >= 0) { + /* If we downloaded something new, go run the callback chain */ + HandbookDownloader.this.mDLFiniCB.run(); + } + } + + @Override + protected void onCancelled(Void aVoid) { + super.onCancelled(aVoid); + Log.d(TAG, "Cancel"); + synchronized (HandbookDownloader.this) { + fini(); + } + } + } + + @Override + public void messageArrived(String topic, MqttMessage message) throws Exception { + /* Try to parse the message: URL timestamp checksum */ + String url, checksum_str; + + Log.d(TAG, "Begin processing download via '" + message.toString()); + if (this.downloader != null) { + Log.d(TAG, "Download already in progress; ignoring message"); + return; + } + + try { + Scanner s = new Scanner(message.toString().trim()); + url = s.next(); + s.next(); /* discard timestamp */ + checksum_str = s.next(); + } catch (NoSuchElementException nsee) { + /* Malformed message; give up */ + Log.d(TAG, "Malformed handbook message"); + return; + } + + if (checksum_str.length() != 64) { + /* Malformed message */ + Log.d(TAG, "Malformed checksum"); + return; + } + + byte[] checksum = new byte[checksum_str.length() / 2]; + for (int i = 0; i < checksum.length; i++) { + checksum[i] = (byte)((Character.digit(checksum_str.charAt(2*i), 16) << 4) + + (Character.digit(checksum_str.charAt(2*i+1),16))); + } + synchronized (this) { + if (lastFetchedChecksum != null + && java.util.Arrays.equals(checksum, lastFetchedChecksum)) { + /* Nothing to do */ + Log.d(TAG, "Checksum matches last fetch"); + return; + } + + if (this.downloader != null) { + Log.d(TAG, "Download already in progress; ignoring message"); + return; + } + + this.downloader = new Task(mMqc); + this.download = new CheckedAsyncDownloader.DL(new URL(url), checksum, + new File(mCtx.getFilesDir(), HandbookActivity.HAND_FILE_NAME)); + this.downloader.execute(this.download); + } + } + + private void subscribe(IMqttAsyncClient mqc) throws MqttException { + mqc.subscribe("ctfws/rules/handbook/html", 2, this); + } + + public void subscribeOn(IMqttAsyncClient mqc) throws MqttException { + synchronized (this) { + mMqc = mqc; + subscribe(mqc); + } + } + + private void unsubscribe(IMqttAsyncClient mqc) throws MqttException { + mqc.unsubscribe("ctfws/rules/handbook/html"); + } + + public void unsubscribeOn(IMqttAsyncClient mqc) throws MqttException { + synchronized (this) { + unsubscribe(mqc); + mMqc = null; + } + } +} \ No newline at end of file diff --git a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainActivity.java b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainActivity.java index 797766c..103e73a 100644 --- a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainActivity.java +++ b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainActivity.java @@ -56,6 +56,11 @@ public class MainActivity extends AppCompatActivity { } } } + + @Override + public void onHandbookFetch(MainService.LocalBinder b) { + ; + } }; private CtFwSDisplayLocal mCdl; // set in onStart diff --git a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainService.java b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainService.java index 71d32b5..6a28839 100644 --- a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainService.java +++ b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainService.java @@ -58,6 +58,15 @@ public class MainService extends Service { public MainService() { } + // Handbook fetch logic; this is a singleton for the service, even as connections come and go. + private HandbookDownloader mHandDL = new HandbookDownloader(this, + new Runnable() { + @Override + public void run() { + MainService.this.notifyHandbook(); + } + }); + // MQTT client management private MqttAndroidClient mMqc; @@ -131,6 +140,8 @@ public class MainService extends Service { setMSE(MqttServerEvent.MSE_SUB); } }); + + mHandDL.subscribeOn(mMqc); } catch (MqttException e) { Log.e("CtFwS", "Exn Sub", e); } @@ -206,6 +217,7 @@ public class MainService extends Service { p + "config", p + "endtime", p + "flags", p + "timesync", p + "message", p + "message/player", p + "message/reset" }); + mHandDL.unsubscribeOn(mMqc); } catch (MqttException me) { Log.d("Service", "domqtt discon unsub exn"); // *&@#&^*#@#&@#&@# @@ -310,6 +322,7 @@ public class MainService extends Service { public interface Observer { void onMqttServerChanged(LocalBinder b, String sURL); void onMqttServerEvent(LocalBinder b, MqttServerEvent mse); + void onHandbookFetch(LocalBinder b); } private final Set mObsvs = new HashSet<>(); private void setMSE(MqttServerEvent mse) { @@ -323,6 +336,11 @@ public class MainService extends Service { for (Observer o : mObsvs) { o.onMqttServerChanged(mBinder, sURL); } } } + private void notifyHandbook() { + synchronized(this) { + for (Observer o : mObsvs) { o.onHandbookFetch(mBinder); } + } + } @Override public void onCreate() { diff --git a/mobile/src/main/res/layout/activity_handbook.xml b/mobile/src/main/res/layout/activity_handbook.xml index cd08b3b..2d2c049 100644 --- a/mobile/src/main/res/layout/activity_handbook.xml +++ b/mobile/src/main/res/layout/activity_handbook.xml @@ -1,5 +1,5 @@ - + android:layout_width="match_parent" + android:layout_height="match_parent"> - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml index 07ed72a..90752dd 100644 --- a/mobile/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -76,11 +76,5 @@ ]]> - - Terribly sorry, something has gone wrong. Please try loading the - official rules. - - ]]> - + Fetched updated handbook!