+# Returns true if the two files differ (by running "cmp")
+#
+sub cmp_files($$) {
+ my ($file1, $file2) = @_;
+
+ my @cmd = ("cmp", "-s", "$file1", "$file2");
+ print STDERR "$progname: executing \"" . join(" ", @cmd) . "\"\n"
+ if ($verbose > 3);
+
+ system (@cmd);
+ my $exit_value = $? >> 8;
+ my $signal_num = $? & 127;
+ my $dumped_core = $? & 128;
+
+ error ("$cmd[0]: core dumped!") if ($dumped_core);
+ error ("$cmd[0]: signal $signal_num!") if ($signal_num);
+ return $exit_value;
+}
+
+
+sub diff_files($$) {
+ my ($file1, $file2) = @_;
+
+ my @cmd = ("diff",
+ "-U1",
+# "-w",
+ "--unidirectional-new-file", "$file1", "$file2");
+ print STDERR "$progname: executing \"" . join(" ", @cmd) . "\"\n"
+ if ($verbose > 3);
+
+ system (@cmd);
+ my $exit_value = $? >> 8;
+ my $signal_num = $? & 127;
+ my $dumped_core = $? & 128;
+
+ error ("$cmd[0]: core dumped!") if ($dumped_core);
+ error ("$cmd[0]: signal $signal_num!") if ($signal_num);
+ return $exit_value;
+}
+
+
+# If the two files differ:
+# mv file2 file1
+# else
+# rm file2
+#
+sub rename_or_delete($$;$) {
+ my ($file, $file_tmp, $suffix_msg) = @_;
+
+ my $changed_p = cmp_files ($file, $file_tmp);
+
+ if ($changed_p && $debug_p) {
+ print STDOUT "\n" . ('#' x 79) . "\n";
+ diff_files ("$file", "$file_tmp");
+ $changed_p = 0;
+ }
+
+ if ($changed_p) {
+
+ if (!rename ("$file_tmp", "$file")) {
+ unlink "$file_tmp";
+ error ("mv $file_tmp $file: $!");
+ }
+ print STDERR "$progname: wrote $file" .
+ ($suffix_msg ? " $suffix_msg" : "") . "\n";
+
+ } else {
+ unlink "$file_tmp" || error ("rm $file_tmp: $!\n");
+ print STDERR "$file unchanged" .
+ ($suffix_msg ? " $suffix_msg" : "") . "\n"
+ if ($verbose);
+ print STDERR "$progname: rm $file_tmp\n" if ($verbose > 2);
+ }
+}
+
+
+# Write the given body to the file, but don't alter the file's
+# date if the new content is the same as the existing content.
+#
+sub write_file_if_changed($$;$) {
+ my ($outfile, $body, $suffix_msg) = @_;
+
+ my $file_tmp = "$outfile.tmp";
+ open (my $out, '>', $file_tmp) || error ("$file_tmp: $!");
+ (print $out $body) || error ("$file_tmp: $!");
+ close $out || error ("$file_tmp: $!");
+ rename_or_delete ($outfile, $file_tmp, $suffix_msg);
+}
+
+
+# Read the template file and splice in the @KEYWORDS@ in the hash.
+#
+sub read_template($$) {
+ my ($file, $subs) = @_;
+ my $body = '';
+ open (my $in, '<', $file) || error ("$file: $!");
+ while (<$in>) { $body .= $_; }
+ close $in;
+
+ $body =~ s@/\*.*?\*/@@gs; # omit comments
+ $body =~ s@//.*$@@gm;
+
+ foreach my $key (keys %$subs) {
+ my $val = $subs->{$key};
+ $body =~ s/@\Q$key\E@/$val/gs;
+ }
+
+ if ($body =~ m/(@[-_A-Z\d]+@)/s) {
+ error ("$file: unmatched: $1 [$body]");
+ }
+
+ $body =~ s/[ \t]+$//gm;
+ $body =~ s/(\n\n)\n+/$1/gs;
+ return $body;
+}
+
+
+# This is duplicated in OSX/update-info-plist.pl
+#
+sub munge_blurb($$$$) {
+ my ($filename, $name, $vers, $desc) = @_;
+
+ $desc =~ s/^([ \t]*\n)+//s;
+ $desc =~ s/\s*$//s;
+
+ # in case it's done already...
+ $desc =~ s@<!--.*?-->@@gs;
+ $desc =~ s/^.* version \d[^\n]*\n//s;
+ $desc =~ s/^From the XScreenSaver.*\n//m;
+ $desc =~ s@^https://www\.jwz\.org/xscreensaver.*\n@@m;
+ $desc =~
+ s/\nCopyright [^ \r\n\t]+ (\d{4})(-\d{4})? (.*)\.$/\nWritten $3; $1./s;
+ $desc =~ s/^\n+//s;
+
+ error ("$filename: description contains markup: $1")
+ if ($desc =~ m/([<>&][^<>&\s]*)/s);
+ error ("$filename: description contains ctl chars: $1")
+ if ($desc =~ m/([\000-\010\013-\037])/s);
+
+ error ("$filename: can't extract authors")
+ unless ($desc =~ m@^(.*)\nWritten by[ \t]+(.+)$@s);
+ $desc = $1;
+ my $authors = $2;
+ $desc =~ s/\s*$//s;
+
+ my $year = undef;
+ if ($authors =~ m@^(.*?)\s*[,;]\s+(\d\d\d\d)([-\s,;]+\d\d\d\d)*[.]?$@s) {
+ $authors = $1;
+ $year = $2;
+ }
+
+ error ("$filename: can't extract year") unless $year;
+ my $cyear = 1900 + ((localtime())[5]);
+ $year = "$cyear" unless $year;
+ if ($year && ! ($year =~ m/$cyear/)) {
+ $year = "$year-$cyear";
+ }
+
+ $authors =~ s/[.,;\s]+$//s;
+
+ # List me as a co-author on all of them, since I'm the one who
+ # did the OSX port, packaged it up, and built the executables.
+ #
+ my $curator = "Jamie Zawinski";
+ if (! ($authors =~ m/$curator/si)) {
+ if ($authors =~ m@^(.*?),? and (.*)$@s) {
+ $authors = "$1, $2, and $curator";
+ } else {
+ $authors .= " and $curator";
+ }
+ }
+
+ my $desc1 = ("$name, version $vers.\n\n" . # savername.xml
+ $desc . "\n" .
+ "\n" .
+ "From the XScreenSaver collection: " .
+ "https://www.jwz.org/xscreensaver/\n" .
+ "Copyright \302\251 $year by $authors.\n");
+
+ my $desc2 = ("$name $vers,\n" . # Info.plist
+ "\302\251 $year $authors.\n" .
+ #"From the XScreenSaver collection:\n" .
+ #"https://www.jwz.org/xscreensaver/\n" .
+ "\n" .
+ $desc .
+ "\n");
+
+ # unwrap lines, but only when it's obviously ok: leave blank lines,
+ # and don't unwrap if that would compress leading whitespace on a line.
+ #
+ $desc2 =~ s/^(From |https?:)/\n$1/gm;
+ 1 while ($desc2 =~ s/([^\s])[ \t]*\n([^\s])/$1 $2/gs);
+ $desc2 =~ s/\n\n(From |https?:)/\n$1/gs;
+
+ return ($desc1, $desc2);
+}
+
+
+sub build_android(@) {
+ my (@savers) = @_;
+
+ my $package = "org.jwz.xscreensaver";
+ my $project_dir = "project/xscreensaver";
+ my $xml_dir = "$project_dir/res/xml";
+ my $values_dir = "$project_dir/res/values";
+ my $java_dir = "$project_dir/src/org/jwz/xscreensaver/gen";
+ my $gen_dir = "gen";
+
+ my $xml_header = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
+
+ my $manifest = '';
+ my $arrays = '';
+ my $strings = '';
+ my %write_files;
+ my %string_dups;
+
+ my $vers;
+ {
+ my $file = "../utils/version.h";
+ my $body = '';
+ open (my $in, '<', $file) || error ("$file: $!");
+ while (<$in>) { $body .= $_; }
+ close $in;
+ ($vers) = ($body =~ m@ (\d+\.\d+) @s);
+ error ("$file: no version number") unless $vers;
+ }
+
+
+ foreach my $saver (@savers) {
+ next if ($saver =~ m/(-helper)$/);
+ $saver = 'rdbomb' if ($saver eq 'rd-bomb');
+
+ my ($src_opts, $switchmap) = parse_src ($saver);
+ my ($saver_title, $gl_p, $xml_opts, $widgets) =
+ parse_xml ($saver, $switchmap, $src_opts);
+
+ my $daydream_class = "${saver_title}Daydream";
+ my $settings_class = "${saver_title}Settings";
+ foreach ($settings_class, $daydream_class) {
+ s/\s+//gs;
+ s/^([a-z])/\U$1/gs; # upcase first letter
+ }
+
+ $saver_title =~ s/(.[a-z])([A-Z\d])/$1 $2/gs; # Spaces in InterCaps
+ $saver_title =~ s/^(GL|RD)[- ]?(.)/$1 \U$2/gs; # Space after "GL"
+ $saver_title =~ s/^Apple ?2$/Apple ][/gs; # "Apple ]["
+ $saver_title =~ s/(m)oe(bius)/$1ö$2/gsi; # ö
+ $saver_title =~ s/(moir)e/$1é/gsi; # é
+ $saver_title =~ s/^([a-z])/\U$1/s; # "M6502" for sorting
+
+ my $settings = '';
+
+ my $localize0 = sub($$) {
+ my ($key, $string) = @_;
+ $string =~ s@([\\\"\'])@\\$1@gs; # backslashify
+ $string =~ s@\n@\\n@gs; # quote newlines
+ $key =~ s@[^a-z\d_]+@_@gsi; # illegal characters
+
+ my $old = $string_dups{$key};
+ error ("dup string: $key: \"$old\" != \"$string\"")
+ if (defined($old) && $old ne $string);
+ $string_dups{$key} = $string;
+
+ my $fmt = ($string =~ m/%/ ? ' formatted="false"' : '');
+ $strings .= "<string name=\"${key}\"$fmt>$string</string>\n"
+ unless defined($old);
+ return "\@string/$key";
+ };
+
+ $localize0->('app_name', 'XScreenSaver');
+
+ $settings .= ("<Preference\n" .
+ " android:key=\"${saver}_reset\"\n" .
+ " android:title=\"" .
+ $localize0->('reset_to_defaults', 'Reset to defaults') .
+ "\"\n" .
+ " />\n");
+
+ my $daydream_desc = '';
+ foreach my $widget (@$widgets) {
+ my $type = $widget->{type};
+ my $rsrc = $widget->{resource};
+ my $label = $widget->{_label};
+ my $def = $widget->{default};
+ my $invert_p = (($widget->{convert} || '') eq 'invert');
+
+ my $key = "${saver}_$rsrc" if $rsrc;
+
+ #### The menus don't actually have titles on X11 or Cocoa...
+ $label = $widget->{resource} unless $label;
+
+ my $localize = sub($;$) {
+ my ($string, $suf) = @_;
+ $suf = 'title' unless $suf;
+ return $localize0->("${saver}_${rsrc}_${suf}", $string);
+ };
+
+ if ($type eq 'slider' || $type eq 'spinbutton') {
+
+ my $low = $widget->{low};
+ my $high = $widget->{high};
+ my $float_p = $low =~ m/[.]/;
+ my $low_label = $widget->{'_low-label'};
+ my $high_label = $widget->{'_high-label'};
+
+ $low_label = $low unless defined($low_label);
+ $high_label = $high unless defined($high_label);
+
+ ($low, $high) = ($high, $low)
+ if (($widget->{convert} || '') eq 'invert');
+
+ $settings .=
+ ("<$package.SliderPreference\n" .
+ " android:layout=\"\@layout/slider_preference\"\n" .
+ " android:key=\"${key}\"\n" .
+ " android:title=\"" . $localize->($label) . "\"\n" .
+ " android:defaultValue=\"$def\"\n" .
+ " low=\"$low\"\n" .
+ " high=\"$high\"\n" .
+ " lowLabel=\"" . $localize->($low_label, 'low_label') . "\"\n" .
+ " highLabel=\"" . $localize->($high_label, 'high_label') . "\"\n" .
+ " integral=\"" .($float_p ? 'false' : 'true'). "\" />\n");
+
+ } elsif ($type eq 'boolean') {
+
+ my $def = ($invert_p ? 'true' : 'false');
+ $settings .=
+ ("<CheckBoxPreference\n" .
+ " android:key=\"${key}\"\n" .
+ " android:title=\"" . $localize->($label) . "\"\n" .
+ " android:defaultValue=\"$def\" />\n");
+
+ } elsif ($type eq 'select') {
+
+ $label =~ s/^(.)/\U$1/s; # upcase first letter of menu title
+ $label =~ s/[-_]/ /gs;
+ $label =~ s/([a-z])([A-Z])/$1 $2/gs;
+ $def = '' unless defined ($def);
+ $settings .=
+ ("<ListPreference\n" .
+ " android:key=\"${key}\"\n" .
+ " android:title=\"" . $localize->($label, 'menu') . "\"\n" .
+ " android:entries=\"\@array/${key}_entries\"\n" .
+ " android:defaultValue=\"$def\"\n" .
+ " android:entryValues=\"\@array/${key}_values\" />\n");
+
+ my $a1 = '';
+ foreach my $item (@{$widget->{menu}}) {
+ my $val = $item->{value};
+ if (! defined($val)) {
+ $val = $src_opts->{$widget->{resource}};
+ error ("$saver: no default resource in option menu " .
+ $item->{_label})
+ unless defined($val);
+ }
+ $val =~ s@([\\\"\'])@\\$1@gs; # backslashify
+ $a1 .= " <item>$val</item>\n";
+ }
+
+ my $a2 = '';
+ foreach my $item (@{$widget->{menu}}) {
+ my $val = $item->{value};
+ $val = $src_opts->{$widget->{resource}} unless defined($val);
+ $a2 .= (" <item>" . $localize->($item->{_label}, $val) .
+ "</item>\n");
+ }
+
+ my $fmt1 = ($a1 =~ m/%/ ? ' formatted="false"' : '');
+ my $fmt2 = ($a2 =~ m/%/ ? ' formatted="false"' : '');
+ $arrays .= ("<string-array name=\"${key}_values\"$fmt1>\n" .
+ $a1 .
+ "</string-array>\n" .
+ "<string-array name=\"${key}_entries\"$fmt2>\n" .
+ $a2 .
+ "</string-array>\n");
+
+ } elsif ($type eq 'string') {
+
+ $def =~ s/&/&/gs;
+ $settings .=
+ ("<EditTextPreference\n" .
+ " android:key=\"${key}\"\n" .
+ " android:title=\"" . $localize->($label) . "\"\n" .
+ " android:defaultValue=\"$def\" />\n");
+
+ } elsif ($type eq 'file') {
+
+ } elsif ($type eq '_description') {
+
+ $type = 'description';
+ $rsrc = $type;
+ my $desc = $widget->{text};
+ (undef, $desc) = munge_blurb ($saver, $saver_title, $vers, $desc);
+
+ # Lose the Wikipedia URLs.
+ $desc =~ s@https?:.*?\b(wikipedia|mathworld)\b[^\s]+[ \t]*\n?@@gm;
+ $desc =~ s/(\n\n)\n+/$1/s;
+ $desc =~ s/\s*$/\n\n\n/s;
+
+ $daydream_desc = $desc;
+
+ my ($year) = ($daydream_desc =~ m/\b((19|20)\d\d)\b/s);
+ error ("$saver: no year") unless $year;
+ $daydream_desc =~ s/^.*?\n\n//gs;
+ $daydream_desc =~ s/\n.*$//gs;
+ $daydream_desc = "$year: $daydream_desc";
+ $daydream_desc =~ s/^(.{72}).+$/$1.../s;
+
+ $settings .=
+ ("<Preference\n" .
+ " android:icon=\"\@drawable/thumbnail\"\n" .
+ " android:key=\"${saver}_${type}\"\n" .
+# " android:selectable=\"false\"\n" .
+ " android:persistent=\"false\"\n" .
+ " android:layout=\"\@layout/preference_blurb\"\n" .
+ " android:summary=\"" . $localize->($desc) . "\">\n" .
+ " <intent android:action=\"android.intent.action.VIEW\"\n" .
+ " android:data=\"https://www.jwz.org/xscreensaver/\" />\n" .
+ "</Preference>\n");
+
+ } else {
+ error ("unhandled type: $type");
+ }
+ }
+
+ my $heading = "XScreenSaver: $saver_title";
+
+ $settings =~ s/^/ /gm;
+ $settings = ($xml_header .
+ "<PreferenceScreen xmlns:android=\"" .
+ "http://schemas.android.com/apk/res/android\"\n" .
+ " android:title=\"" .
+ $localize0->("${saver}_settings_title", $heading) . "\">\n" .
+ $settings .
+ "</PreferenceScreen>\n");
+
+ my $saver_underscore = $saver;
+ $saver_underscore =~ s/-/_/g;
+ $write_files{"$xml_dir/${saver_underscore}_settings.xml"} = $settings;
+
+ $manifest .= ("<service android:label=\"" .
+ $localize0->("${saver_underscore}_saver_title",
+ $saver_title) .
+ "\"\n" .
+ " android:summary=\"" .
+ $localize0->("${saver_underscore}_saver_desc",
+ $daydream_desc) . "\"\n" .
+ " android:name=\".gen.$daydream_class\"\n" .
+ " android:permission=\"android.permission" .
+ ".BIND_DREAM_SERVICE\"\n" .
+ " android:exported=\"true\"\n" .
+ " android:icon=\"\@drawable/${saver_underscore}\">\n" .
+ " <intent-filter>\n" .
+ " <action android:name=\"android.service.dreams" .
+ ".DreamService\" />\n" .
+ " <category android:name=\"android.intent.category" .
+ ".DEFAULT\" />\n" .
+ " </intent-filter>\n" .
+ " <meta-data android:name=\"android.service.dream\"\n" .
+ " android:resource=\"\@xml/${saver}_dream\" />\n" .
+ "</service>\n" .
+ "<activity android:name=\"" .
+ "$package.gen.$settings_class\" />\n"
+ );
+
+ my $dream = ("<dream xmlns:android=\"" .
+ "http://schemas.android.com/apk/res/android\"\n" .
+ " android:settingsActivity=\"" .
+ "$package.gen.$settings_class\" />\n");
+ $write_files{"$xml_dir/${saver_underscore}_dream.xml"} = $dream;
+
+ $write_files{"$java_dir/$daydream_class.java"} =
+ read_template ("XScreenSaverDaydream.java.in",
+ { CLASS => $daydream_class,
+ API => ($gl_p ? 'GL' : 'XLIB') });
+
+ $write_files{"$java_dir/$settings_class.java"} =
+ read_template ("XScreenSaverSettings.java.in",
+ { CLASS => $settings_class });
+ }
+
+ $arrays =~ s/^/ /gm;
+ $arrays = ($xml_header .
+ "<resources xmlns:xliff=\"" .
+ "urn:oasis:names:tc:xliff:document:1.2\">\n" .
+ $arrays .
+ "</resources>\n");
+
+ $strings =~ s/^/ /gm;
+ $strings = ($xml_header .
+ "<resources>\n" .
+ $strings .
+ "</resources>\n");
+
+ $manifest .= "<activity android:name=\"$package.XScreenSaverSettings\" />\n";
+
+ $manifest .= ("<activity android:name=\"" .
+ "org.jwz.xscreensaver.XScreenSaverActivity\"\n" .
+ " android:theme=\"\@android:style/Theme.Holo\"\n" .
+ " android:label=\"\@string/app_name\">\n" .
+ " <intent-filter>\n" .
+ " <action android:name=\"android.intent.action" .
+ ".MAIN\" />\n" .
+ " <category android:name=\"android.intent.category" .
+ ".LAUNCHER\" />\n" .
+ " </intent-filter>\n" .
+ " <intent-filter>\n" .
+ " <action android:name=\"android.intent.action" .
+ ".VIEW\" />\n" .
+ " <category android:name=\"android.intent.category" .
+ ".DEFAULT\" />\n" .
+ " <category android:name=\"android.intent.category" .
+ ".BROWSABLE\" />\n" .
+ " </intent-filter>\n" .
+ "</activity>\n");
+
+ # Android wants this to be an int
+ my $versb = $vers;
+ $versb =~ s/^(\d+)\.(\d+).*$/{ $1 * 10000 + $2 * 100 }/sex;
+ $versb++ if ($versb == 53500); # Herp derp
+
+ $manifest =~ s/^/ /gm;
+ $manifest = ($xml_header .
+ "<manifest xmlns:android=\"" .
+ "http://schemas.android.com/apk/res/android\"\n" .
+ " package=\"$package\"\n" .
+ " android:versionCode=\"$versb\"\n" .
+ " android:versionName=\"$vers\">\n" .
+
+ " <uses-sdk android:minSdkVersion=\"14\"" .
+ " android:targetSdkVersion=\"19\" />\n" .
+
+ " <uses-feature android:glEsVersion=\"0x00010001\"\n" .
+ " android:required=\"true\" />\n" .
+
+ " <uses-permission android:name=\"" .
+ "android.permission.INTERNET\" />\n" .
+ " <uses-permission android:name=\"" .
+ "android.permission.READ_EXTERNAL_STORAGE\" />\n" .
+
+ " <application android:icon=\"\@drawable/thumbnail\"\n" .
+ " android:label=\"\@string/app_name\"\n" .
+ " android:name=\".XScreenSaverApp\">\n" .
+ $manifest .
+ " </application>\n" .
+ "</manifest>\n");
+
+ $write_files{"$project_dir/AndroidManifest.xml"} = $manifest;
+ $write_files{"$values_dir/settings.xml"} = $arrays;
+ $write_files{"$values_dir/strings.xml"} = $strings;
+
+ my @s2 = ();
+ foreach my $saver (sort @savers) {
+ push @s2, $saver unless ($saver =~ m/(-helper)$/);
+ }
+ my @s3 = @s2;
+
+ foreach (@s2) { s/^(.*)$/${1}_xscreensaver_function_table/s; }
+ foreach (@s3) { s/^(.*)$/{"$1", &${1}_xscreensaver_function_table}/s; }
+
+ my $fntable_h = ("extern struct xscreensaver_function_table\n" .
+ " " . join(",\n ", @s2) . ";\n" .
+ "\n" .
+ "static const struct function_table_entry" .
+ " function_table[] = {\n" .
+ " " . join(",\n ", @s3) . "\n" .
+ "};\n");
+ $write_files{"$gen_dir/function-table.h"} = $fntable_h;
+
+
+ $write_files{"$values_dir/attrs.xml"} =
+ # This file doesn't actually have any substitutions in it, so it could
+ # just be static, somewhere...
+ # SliderPreference.java refers to this via "R.styleable.SliderPreference".
+ ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" .
+ "<resources>\n" .
+ " <declare-styleable name=\"SliderPreference\">\n" .
+ " <attr name=\"android:summary\" />\n" .
+ " </declare-styleable>\n" .
+ "</resources>\n");
+
+
+ foreach my $file (sort keys %write_files) {
+ my ($dir) = ($file =~ m@^(.*)/[^/]*$@s);
+ system ("mkdir", "-p", $dir) if (! -d $dir && !$debug_p);
+ my $body = $write_files{$file};
+ $body = "// Generated by $progname\n$body"
+ if ($file =~ m/\.(java|[chm])$/s);
+ write_file_if_changed ($file, $body);
+ }
+
+ # Unlink any .xml files from a previous run that shouldn't be there:
+ # if a hack is removed from $ANDROID_HACKS in android/Makefile but
+ # the old XML files remain behind, the build blows up.
+ #
+ opendir (my $dirp, $xml_dir) || error ("$xml_dir: $!");
+ my @files = readdir ($dirp);
+ closedir $dirp;
+ foreach my $f (sort @files) {
+ next if ($f eq '.' || $f eq '..');
+ $f = "$xml_dir/$f";
+ next if (defined ($write_files{$f}));
+ if ($f =~ m/_(settings|dream)\.xml$/s) {
+ print STDERR "$progname: rm $f\n";
+ unlink ($f) unless ($debug_p);
+ } else {
+ print STDERR "$progname: warning: unrecognised file: $f\n";
+ }
+ }
+}
+
+