Merge branch 'android_branch' into android_translate

This commit is contained in:
Eric House 2019-01-09 19:03:21 -08:00
commit 1625ec9062
116 changed files with 2778 additions and 1675 deletions

View file

@ -1,8 +1,10 @@
git:
depth: false
language: android language: android
jdk: oraclejdk8 jdk: oraclejdk8
env: env:
global: global:
- ANDROID_TARGET=android-15 - ANDROID_TARGET=android-14
- ANDROID_ABI=armeabi-v7a,x86 - ANDROID_ABI=armeabi-v7a,x86
- secure: d8PwteM+xp1IRU3QkvmHtxh+1Ta9n/kl/SJ3EZa3iColVVXY1etzjY3cKrEGKKMJuI4be30kPzvNw9/BVTawDpnU9/NtWqykJ8QHXNWnZIvUQ/kxHBS1DbcstmcYU9gvR83EFb8BT+Y9frpNfMcZDlSvBpEGqDQEPmxiDzSmjdUmJJQWStncxL9pE+lCdM6lHBgtfYoMMiqCQF/DxkQisjyUVF4mbTGuT9JOOWjVsTGPA7ehzsWDHoJ3p2ai8UKHAYucUWZcTt4rkq9l35ExvgKd3L8luk8U3X3Fk9yzVhPJC56T0XNbNrsQ2W7/7oGRv6EQFV3aKDZimJ7CVjBcEjZmPxeUVvCsMW8XB41ZvYcy6xsjF96oyjn1gb0r/2mZbTaWP0izSTwMYZ5vFNKUamDtRZgrneD0lfvXgfTzirrCU7FqO2RH7ZK5PQpSgSoZxKsKyeyFPEa2ihivc95rz1MS6mamle9wrIlSAgEGcaZMIYvKiOnCLk7CZCKuwm2dhYPgzCHW3PUopay59BBwMsSqWpxsiHEr5jYGpb0pHGbzPTJNUpg1LNQX5eMQOMlEt7rfpoC7JG24hR9vxl4Yf9LhxYlSwUiPy7TYHdbA0kUS68skfzxU6+ekWZF2QFM+L4vWCYmEHDy7n+I0df+PavycgNW989ROlAKhQjtMyqM= - secure: d8PwteM+xp1IRU3QkvmHtxh+1Ta9n/kl/SJ3EZa3iColVVXY1etzjY3cKrEGKKMJuI4be30kPzvNw9/BVTawDpnU9/NtWqykJ8QHXNWnZIvUQ/kxHBS1DbcstmcYU9gvR83EFb8BT+Y9frpNfMcZDlSvBpEGqDQEPmxiDzSmjdUmJJQWStncxL9pE+lCdM6lHBgtfYoMMiqCQF/DxkQisjyUVF4mbTGuT9JOOWjVsTGPA7ehzsWDHoJ3p2ai8UKHAYucUWZcTt4rkq9l35ExvgKd3L8luk8U3X3Fk9yzVhPJC56T0XNbNrsQ2W7/7oGRv6EQFV3aKDZimJ7CVjBcEjZmPxeUVvCsMW8XB41ZvYcy6xsjF96oyjn1gb0r/2mZbTaWP0izSTwMYZ5vFNKUamDtRZgrneD0lfvXgfTzirrCU7FqO2RH7ZK5PQpSgSoZxKsKyeyFPEa2ihivc95rz1MS6mamle9wrIlSAgEGcaZMIYvKiOnCLk7CZCKuwm2dhYPgzCHW3PUopay59BBwMsSqWpxsiHEr5jYGpb0pHGbzPTJNUpg1LNQX5eMQOMlEt7rfpoC7JG24hR9vxl4Yf9LhxYlSwUiPy7TYHdbA0kUS68skfzxU6+ekWZF2QFM+L4vWCYmEHDy7n+I0df+PavycgNW989ROlAKhQjtMyqM=
android: android:
@ -10,7 +12,7 @@ android:
- tools - tools
- platform-tools - platform-tools
- build-tools-27.0.3 - build-tools-27.0.3
- android-23 - android-26
licenses: licenses:
- android-sdk-preview-license-.+ - android-sdk-preview-license-.+
- android-sdk-license-.+ - android-sdk-license-.+

View file

@ -1,6 +1,6 @@
def INITIAL_CLIENT_VERS = 8 def INITIAL_CLIENT_VERS = 8
def VERSION_CODE_BASE = 135 def VERSION_CODE_BASE = 136
def VERSION_NAME = '4.4.139' def VERSION_NAME = '4.4.140'
def FABRIC_API_KEY = System.getenv("FABRIC_API_KEY") def FABRIC_API_KEY = System.getenv("FABRIC_API_KEY")
def GCM_SENDER_ID = System.getenv("GCM_SENDER_ID") def GCM_SENDER_ID = System.getenv("GCM_SENDER_ID")
def BUILD_INFO_NAME = "build-info.txt" def BUILD_INFO_NAME = "build-info.txt"
@ -23,8 +23,8 @@ if ( FABRIC_API_KEY && hasProperty('useCrashlytics') ) {
apply plugin: 'io.fabric' apply plugin: 'io.fabric'
} }
repositories { repositories {
maven { url 'https://maven.fabric.io/public' }
google() google()
maven { url 'https://maven.fabric.io/public' }
} }
android { android {
@ -32,8 +32,8 @@ android {
// default changes and .travis.yml can be kept in sync // default changes and .travis.yml can be kept in sync
buildToolsVersion '27.0.3' buildToolsVersion '27.0.3'
defaultConfig { defaultConfig {
minSdkVersion 8 minSdkVersion 14
targetSdkVersion 23 targetSdkVersion 26
versionCode VERSION_CODE_BASE versionCode VERSION_CODE_BASE
versionName VERSION_NAME versionName VERSION_NAME
} }
@ -63,6 +63,7 @@ android {
productFlavors { productFlavors {
all { all {
buildConfigField "String", "BUILD_INFO_NAME", "\"${BUILD_INFO_NAME}\"" buildConfigField "String", "BUILD_INFO_NAME", "\"${BUILD_INFO_NAME}\""
resValue "string", "invite_prefix", "/and/"
} }
xw4 { xw4 {
@ -71,7 +72,6 @@ android {
manifestPlaceholders = [ APP_ID: applicationId ] manifestPlaceholders = [ APP_ID: applicationId ]
resValue "string", "app_name", "CrossWords" resValue "string", "app_name", "CrossWords"
resValue "string", "nbs_port", "3344" resValue "string", "nbs_port", "3344"
resValue "string", "invite_prefix", "/and/"
buildConfigField "boolean", "WIDIR_ENABLED", "false" buildConfigField "boolean", "WIDIR_ENABLED", "false"
buildConfigField "boolean", "RELAYINVITE_SUPPORTED", "false" buildConfigField "boolean", "RELAYINVITE_SUPPORTED", "false"
@ -84,7 +84,6 @@ android {
manifestPlaceholders = [ APP_ID: applicationId ] manifestPlaceholders = [ APP_ID: applicationId ]
resValue "string", "app_name", "CrossWords" resValue "string", "app_name", "CrossWords"
resValue "string", "nbs_port", "3344" resValue "string", "nbs_port", "3344"
resValue "string", "invite_prefix", "/and/"
buildConfigField "boolean", "WIDIR_ENABLED", "false" buildConfigField "boolean", "WIDIR_ENABLED", "false"
buildConfigField "boolean", "RELAYINVITE_SUPPORTED", "false" buildConfigField "boolean", "RELAYINVITE_SUPPORTED", "false"
@ -96,7 +95,6 @@ android {
manifestPlaceholders = [ FABRIC_API_KEY: "$FABRIC_API_KEY", APP_ID: applicationId, ] manifestPlaceholders = [ FABRIC_API_KEY: "$FABRIC_API_KEY", APP_ID: applicationId, ]
resValue "string", "app_name", "CrossDbg" resValue "string", "app_name", "CrossDbg"
resValue "string", "nbs_port", "3345" resValue "string", "nbs_port", "3345"
resValue "string", "invite_prefix", "/anddbg/"
buildConfigField "boolean", "WIDIR_ENABLED", "true" buildConfigField "boolean", "WIDIR_ENABLED", "true"
buildConfigField "boolean", "RELAYINVITE_SUPPORTED", "true" buildConfigField "boolean", "RELAYINVITE_SUPPORTED", "true"
@ -197,11 +195,19 @@ android {
} }
} }
ext {
SUPPORT_LIB_VERSION = '27.1.1'
}
dependencies { dependencies {
// Look into replacing this with a fetch too PENDING // Look into replacing this with a fetch too PENDING
xw4Implementation files('../libs/gcm.jar') xw4Implementation files('../libs/gcm.jar')
implementation 'com.android.support:support-v4:23.4.0' implementation "com.android.support:support-v4:$SUPPORT_LIB_VERSION"
implementation "com.android.support:support-compat:$SUPPORT_LIB_VERSION"
implementation "android.arch.lifecycle:extensions:1.1.1"
annotationProcessor "android.arch.lifecycle:compiler:1.1.1"
// 2.6.8 is probably as far forward as I can go without upping my // 2.6.8 is probably as far forward as I can go without upping my
// min-supported SDK version // min-supported SDK version
@ -222,7 +228,13 @@ task copyStrings(type: Exec) {
task ndkSetup(type: Exec) { task ndkSetup(type: Exec) {
workingDir '../' workingDir '../'
commandLine "./scripts/ndksetup.sh", "--with-clang", "--arm-only" // remove ', "--arm-only"' for Genymotion builds
// I'm putting ARM back for a while. It's too much trouble having
// builds, including those built by travis, that don't run on the
// emulator. Maybe remove this change before each release?
commandLine "./scripts/ndksetup.sh", "--with-clang"
// commandLine "./scripts/ndksetup.sh", "--with-clang", "--arm-only"
} }
task myPreBuild(dependsOn: ['ndkSetup', 'mkImages', 'copyStrings', 'mkXml']) { task myPreBuild(dependsOn: ['ndkSetup', 'mkImages', 'copyStrings', 'mkXml']) {

View file

@ -24,6 +24,8 @@
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.READ_CONTACTS" /> <uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- Required for wifi-direct --> <!-- Required for wifi-direct -->
<!-- <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> --> <!-- <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> -->
@ -113,6 +115,7 @@
<receiver android:name="OnBootReceiver"> <receiver android:name="OnBootReceiver">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/> <action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name="RelayReceiver"/> <receiver android:name="RelayReceiver"/>
@ -179,9 +182,17 @@
<activity android:name=".loc.LocItemEditActivity" <activity android:name=".loc.LocItemEditActivity"
/> />
<service android:name="RelayService"/> <service android:name="BTService"
<service android:name="BTService"/> android:permission="android.permission.BIND_JOB_SERVICE"
<service android:name="SMSService"/> android:exported="false"
/>
<service android:name="SMSService"
android:exported="false"
/>
<service android:name="RelayService"
android:exported="false"
android:permission="android.permission.BIND_JOB_SERVICE"
/>
<receiver android:name=".MountEventReceiver"> <receiver android:name=".MountEventReceiver">
<intent-filter> <intent-filter>
@ -201,6 +212,9 @@
<intent-filter> <intent-filter>
<action android:name="android.bluetooth.adapter.action.STATE_CHANGED" /> <action android:name="android.bluetooth.adapter.action.STATE_CHANGED" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="org.eehouse.android.ACTION_STOP_BT" />
</intent-filter>
</receiver> </receiver>
<receiver android:name="SMSReceiver" > <receiver android:name="SMSReceiver" >

View file

@ -13,9 +13,9 @@
</style> </style>
</head> </head>
<body> <body>
<h2>CrossWords 4.4.139 release</h2> <h2>CrossWords 4.4.140 release</h2>
<p>This release fixes a bug with picking tiles face-up</p> <p>This release updates translations for Catalan, French, German and Japanese.</p>
<div id="survey"> <div id="survey">
<p>Please <a href="https://www.surveymonkey.com/s/GX3XLHR">take <p>Please <a href="https://www.surveymonkey.com/s/GX3XLHR">take
@ -25,19 +25,18 @@
<h3>New with this release</h3> <h3>New with this release</h3>
<ul> <ul>
<li>Don't put up multiple tile pickers. (This feature is rarely <li>Include changes made by Weblate volunteers for four
used, but had apparently been broken for a while.)</li> languages. (Thanks!)</li>
<li>Fix two more bugs reported by users (Thanks!)</li>
<li>Include new translations for Catalan and Norwegian</li>
</ul> </ul>
<p>(The full changelog <p>(The full changelog
is <a href="http://xwords.sf.net/and_changes.php">here</a>.)</p> is <a href="http://xwords.sf.net/and_changes.php">here</a>.)</p>
<h3>Next up</h3> <h3>Next up</h3>
<ul> <ul>
<li>Continue work to support WiFi Direct (currently working <li>Make changes in how background networking and bluetooth work
sporadically)</li> that Google's mandating for all releases after 1
November.</li>
</ul> </ul>
<p>Please let me know <p>Please let me know

View file

@ -33,7 +33,6 @@ import android.widget.TextView;
import java.text.DateFormat; import java.text.DateFormat;
import java.util.Date; import java.util.Date;
import junit.framework.Assert;
import org.eehouse.android.xw4.loc.LocUtils; import org.eehouse.android.xw4.loc.LocUtils;

View file

@ -0,0 +1,58 @@
/* -*- compile-command: "find-and-gradle.sh insXw4Deb"; -*- */
/*
* Copyright 2012 by Eric House (xwords@eehouse.org). All rights
* reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This program 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
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package org.eehouse.android.xw4;
public class Assert {
private static final String TAG = Assert.class.getSimpleName();
public static void fail() {
assertTrue(false);
}
public static void assertFalse(boolean val)
{
assertTrue(! val);
}
public static void assertTrue(boolean val) {
if (! val) {
Log.e( TAG, "firing assert!" );
DbgUtils.printStack( TAG );
junit.framework.Assert.fail();
}
}
public static void assertNotNull( Object val )
{
assertTrue( val != null );
}
public static void assertNull( Object val )
{
assertTrue( val == null );
}
public static void assertEquals( Object obj1, Object obj2 )
{
assertTrue( (obj1 == null && obj2 == null)
|| (obj1 != null && obj1.equals(obj2)) );
}
}

View file

@ -1,6 +1,6 @@
/* -*- compile-command: "find-and-gradle.sh insXw4Deb"; -*- */ /* -*- compile-command: "find-and-gradle.sh insXw4Deb"; -*- */
/* /*
* Copyright 2009 - 2016 by Eric House (xwords@eehouse.org). All rights * Copyright 2009 - 2019 by Eric House (xwords@eehouse.org). All rights
* reserved. * reserved.
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -22,26 +22,87 @@ package org.eehouse.android.xw4;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.bluetooth.BluetoothDevice;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.text.format.DateUtils;
import android.view.View; import android.view.View;
import android.widget.Button; import android.widget.Button;
import junit.framework.Assert;
import org.eehouse.android.xw4.DBUtils.SentInvitesInfo; import org.eehouse.android.xw4.DBUtils.SentInvitesInfo;
import org.eehouse.android.xw4.DlgDelegate.Action; import org.eehouse.android.xw4.DlgDelegate.Action;
import java.util.Iterator; import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Set; import java.util.Set;
public class BTInviteDelegate extends InviteDelegate { public class BTInviteDelegate extends InviteDelegate {
private static final int[] BUTTONIDS = { R.id.button_add, private static final String TAG = BTInviteDelegate.class.getSimpleName();
private static final String KEY_PERSIST = TAG + "_persist";
private static final int[] BUTTONIDS = { R.id.button_scan,
R.id.button_settings, R.id.button_settings,
R.id.button_clear, R.id.button_clear,
}; };
private static final boolean ENABLE_FAKER = false;
private Activity m_activity; private Activity m_activity;
private TwoStringPair[] m_pairs; private ProgressDialog m_progress;
private static class Persisted implements Serializable {
List<TwoStringPair> pairs;
// HashMap: m_stamps is serialized, so can't be abstract type
HashMap<String, Long> stamps = new HashMap<>();
void add( String devAddress, String devName ) {
// If it's already there, update it. Otherwise create new
boolean alreadyHave = false;
if ( null == pairs ) {
pairs = new ArrayList<>();
} else {
for ( TwoStringPair pair : pairs ) {
alreadyHave = pair.str2.equals(devName);
if ( alreadyHave ) {
break;
}
}
}
if ( !alreadyHave ) {
pairs.add( new TwoStringPair( devAddress, devName ) );
}
stamps.put( devName, System.currentTimeMillis() );
sort();
}
void remove(final Set<? extends InviterItem> checked)
{
for ( InviterItem item : checked ) {
TwoStringPair pair = (TwoStringPair)item;
stamps.remove( pair.str2 );
pairs.remove( pair );
}
}
boolean empty() { return pairs == null || pairs.size() == 0; }
private void sort()
{
Collections.sort( pairs, new Comparator<TwoStringPair>() {
@Override
public int compare( TwoStringPair rec1, TwoStringPair rec2 ) {
long val1 = stamps.get( rec1.str2 );
long val2 = stamps.get( rec2.str2 );
return (int)(val2 - val1);
}
});
}
}
private Persisted mPersisted;
public static void launchForResult( Activity activity, int nMissing, public static void launchForResult( Activity activity, int nMissing,
SentInvitesInfo info, SentInvitesInfo info,
@ -68,27 +129,38 @@ public class BTInviteDelegate extends InviteDelegate {
@Override @Override
protected void init( Bundle savedInstanceState ) protected void init( Bundle savedInstanceState )
{ {
String msg = getString( R.string.bt_pick_addall_button ); String msg = getQuantityString( R.plurals.invite_bt_desc_fmt_2, m_nMissing,
msg = getQuantityString( R.plurals.invite_bt_desc_fmt, m_nMissing, m_nMissing )
m_nMissing, msg ); + getString( R.string.invite_bt_desc_postscript );
super.init( msg, 0 ); super.init( msg, 0 );
addButtonBar( R.layout.bt_buttons, BUTTONIDS ); addButtonBar( R.layout.bt_buttons, BUTTONIDS );
BTService.clearDevices( m_activity, null ); // will return names
load();
if ( mPersisted.empty() ) {
scan();
} else {
updateListAdapter( mPersisted.pairs );
}
} }
@Override @Override
protected void onBarButtonClicked( int id ) protected void onBarButtonClicked( int id )
{ {
switch( id ) { switch( id ) {
case R.id.button_add: case R.id.button_scan:
scan(); scan();
break; break;
case R.id.button_settings: case R.id.button_settings:
BTService.openBTSettings( m_activity ); BTService.openBTSettings( m_activity );
break; break;
case R.id.button_clear: case R.id.button_clear:
removeSelected(); mPersisted.remove( getChecked() );
store();
clearChecked(); clearChecked();
updateListAdapter( mPersisted.pairs );
tryEnable();
break; break;
} }
} }
@ -101,20 +173,24 @@ public class BTInviteDelegate extends InviteDelegate {
case SCAN_DONE: case SCAN_DONE:
post( new Runnable() { post( new Runnable() {
public void run() { public void run() {
synchronized( BTInviteDelegate.this ) { m_progress.cancel();
m_pairs = null; if ( mPersisted.empty() ) {
if ( 0 < args.length ) { makeNotAgainBuilder( R.string.not_again_emptybtscan,
m_pairs = TwoStringPair.make( (String[])(args[0]), R.string.key_notagain_emptybtscan )
(String[])(args[1]) ); .show();
}
updateListAdapter( m_pairs );
tryEnable();
} }
} }
} ); } );
break; break;
case HOST_PONGED:
post( new Runnable() {
@Override
public void run() {
processScanResult( (BluetoothDevice)args[0] );
}
} );
break;
default: default:
super.eventOccurred( event, args ); super.eventOccurred( event, args );
} }
@ -123,10 +199,18 @@ public class BTInviteDelegate extends InviteDelegate {
@Override @Override
protected void onChildAdded( View child, InviterItem data ) protected void onChildAdded( View child, InviterItem data )
{ {
TwoStrsItem item = (TwoStrsItem)child; String devName = ((TwoStringPair)data).str2;
TwoStringPair pair = (TwoStringPair)data;
// null: we don't display mac address String msg = null;
((TwoStrsItem)child).setStrings( pair.str2, null ); if ( mPersisted.stamps.containsKey( devName ) ) {
CharSequence elapsed = DateUtils
.getRelativeTimeSpanString( mPersisted.stamps.get( devName ),
System.currentTimeMillis(),
DateUtils.SECOND_IN_MILLIS );
msg = getString( R.string.bt_scan_age_fmt, elapsed );
}
((TwoStrsItem)child).setStrings( devName, msg );
} }
@Override @Override
@ -142,9 +226,16 @@ public class BTInviteDelegate extends InviteDelegate {
private void scan() private void scan()
{ {
if ( ENABLE_FAKER && Utils.nextRandomInt() % 5 == 0 ) {
mPersisted.add( "00:00:00:00:00:00", "Do Not Invite Me" );
}
int count = BTService.getPairedCount( m_activity ); int count = BTService.getPairedCount( m_activity );
if ( 0 < count ) { if ( 0 < count ) {
BTService.scan( m_activity ); String msg = getQuantityString( R.plurals.bt_scan_progress_fmt, count, count );
m_progress = ProgressDialog.show( m_activity, msg, null, true, true );
BTService.scan( m_activity, 5000 );
} else { } else {
makeConfirmThenBuilder( R.string.bt_no_devs, makeConfirmThenBuilder( R.string.bt_no_devs,
Action.OPEN_BT_PREFS_ACTION ) Action.OPEN_BT_PREFS_ACTION )
@ -153,17 +244,34 @@ public class BTInviteDelegate extends InviteDelegate {
} }
} }
// @Override private void processScanResult( BluetoothDevice dev )
private void removeSelected()
{ {
Set<InviterItem> checked = getChecked(); DbgUtils.assertOnUIThread();
String[] devs = new String[checked.size()];
Iterator<InviterItem> iter = checked.iterator(); mPersisted.add( dev.getAddress(), dev.getName() );
for ( int ii = 0; iter.hasNext(); ++ii ) { store();
TwoStringPair pair = (TwoStringPair)iter.next();
devs[ii] = pair.str1; updateListAdapter( mPersisted.pairs );
tryEnable();
}
private void load()
{
try {
String str64 = DBUtils.getStringFor( m_activity, KEY_PERSIST, null );
mPersisted = (Persisted)Utils.string64ToSerializable( str64 );
} catch ( Exception ex ) {} // NPE, de-serialization problems, etc.
if ( null == mPersisted ) {
mPersisted = new Persisted();
} }
BTService.clearDevices( m_activity, devs ); }
private void store()
{
String str64 = mPersisted == null
? "" : Utils.serializableToString64( mPersisted );
DBUtils.setStringFor( m_activity, KEY_PERSIST, str64 );
} }
// DlgDelegate.DlgClickNotify interface // DlgDelegate.DlgClickNotify interface

View file

@ -29,6 +29,9 @@ import android.content.Intent;
public class BTReceiver extends BroadcastReceiver { public class BTReceiver extends BroadcastReceiver {
private static final String TAG = BTReceiver.class.getSimpleName(); private static final String TAG = BTReceiver.class.getSimpleName();
// This string is also used in AndroidManifest (as a string literal)
static final String ACTION_STOP_BT = "org.eehouse.android.ACTION_STOP_BT";
@Override @Override
public void onReceive( Context context, Intent intent ) public void onReceive( Context context, Intent intent )
{ {
@ -37,9 +40,14 @@ public class BTReceiver extends BroadcastReceiver {
Log.d( TAG, "BTReceiver.onReceive(action=%s, intent=%s)", Log.d( TAG, "BTReceiver.onReceive(action=%s, intent=%s)",
action, intent.toString() ); action, intent.toString() );
if ( action.equals( BluetoothDevice.ACTION_ACL_CONNECTED ) ) { switch (action ) {
BTService.startService( context ); case ACTION_STOP_BT:
} else if ( action.equals( BluetoothAdapter.ACTION_STATE_CHANGED ) ) { BTService.stopBackground( context );
break;
case BluetoothDevice.ACTION_ACL_CONNECTED:
BTService.onACLConnected( context );
break;
case BluetoothAdapter.ACTION_STATE_CHANGED:
int newState = int newState =
intent.getIntExtra( BluetoothAdapter.EXTRA_STATE, -1 ); intent.getIntExtra( BluetoothAdapter.EXTRA_STATE, -1 );
switch ( newState ) { switch ( newState ) {

View file

@ -27,7 +27,6 @@ import java.net.Socket;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import org.json.JSONObject; import org.json.JSONObject;
import junit.framework.Assert;
public class BiDiSockWrap { public class BiDiSockWrap {
private static final String TAG = BiDiSockWrap.class.getSimpleName(); private static final String TAG = BiDiSockWrap.class.getSimpleName();

View file

@ -31,7 +31,6 @@ import android.graphics.RectF;
import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import junit.framework.Assert;
import org.eehouse.android.xw4.jni.BoardDims; import org.eehouse.android.xw4.jni.BoardDims;
import org.eehouse.android.xw4.jni.CommonPrefs; import org.eehouse.android.xw4.jni.CommonPrefs;

View file

@ -29,7 +29,6 @@ import android.view.View;
import android.view.View.MeasureSpec; import android.view.View.MeasureSpec;
import android.graphics.Rect; import android.graphics.Rect;
import junit.framework.Assert;
public class BoardContainer extends ViewGroup { public class BoardContainer extends ViewGroup {
private static final String TAG = BoardContainer.class.getSimpleName(); private static final String TAG = BoardContainer.class.getSimpleName();

View file

@ -49,7 +49,6 @@ import java.util.Map;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import junit.framework.Assert;
import org.eehouse.android.xw4.DBUtils.SentInvitesInfo; import org.eehouse.android.xw4.DBUtils.SentInvitesInfo;
import org.eehouse.android.xw4.DlgDelegate.Action; import org.eehouse.android.xw4.DlgDelegate.Action;
@ -188,16 +187,14 @@ public class BoardDelegate extends DelegateBase
} }
break; break;
case GAME_OVER: case GAME_OVER: {
case DLG_CONNSTAT: {
GameSummary summary = (GameSummary)params[0]; GameSummary summary = (GameSummary)params[0];
int title = (Integer)params[1]; int title = (Integer)params[1];
String msg = (String)params[2]; String msg = (String)params[2];
ab.setTitle( title ) ab.setTitle( title )
.setMessage( msg ) .setMessage( msg )
.setPositiveButton( android.R.string.ok, null ); .setPositiveButton( android.R.string.ok, null );
if ( DlgID.GAME_OVER == dlgID if ( rematchSupported( m_activity, true, summary ) ) {
&& rematchSupported( m_activity, true, summary ) ) {
lstnr = new OnClickListener() { lstnr = new OnClickListener() {
public void onClick( DialogInterface dlg, public void onClick( DialogInterface dlg,
int whichButton ) { int whichButton ) {
@ -216,25 +213,6 @@ public class BoardDelegate extends DelegateBase
}; };
ab.setNeutralButton( R.string.button_archive, lstnr ); ab.setNeutralButton( R.string.button_archive, lstnr );
} }
} else if ( DlgID.DLG_CONNSTAT == dlgID
&& BuildConfig.DEBUG && null != m_connTypes
&& (m_connTypes.contains( CommsConnType.COMMS_CONN_RELAY )
|| m_connTypes.contains( CommsConnType.COMMS_CONN_P2P )) ) {
lstnr = new OnClickListener() {
public void onClick( DialogInterface dlg,
int whichButton ) {
NetStateCache.reset( m_activity );
if ( m_connTypes.contains( CommsConnType.COMMS_CONN_RELAY ) ) {
RelayService.reset( m_activity );
}
if ( m_connTypes.contains( CommsConnType.COMMS_CONN_P2P ) ) {
WiDirService.reset( m_activity );
}
}
};
ab.setNegativeButton( R.string.button_reconnect, lstnr );
} }
dialog = ab.create(); dialog = ab.create();
} }
@ -536,7 +514,7 @@ public class BoardDelegate extends DelegateBase
finish(); finish();
} }
}; };
alert.setNoDismissListenerNeg( ab, R.string.button_wait, lstnr ); alert.setNoDismissListenerNeg( ab, R.string.button_close, lstnr );
} }
dialog = ab.create(); dialog = ab.create();
@ -588,17 +566,21 @@ public class BoardDelegate extends DelegateBase
m_haveInvited = args.getBoolean( GameUtils.INVITED, false ); m_haveInvited = args.getBoolean( GameUtils.INVITED, false );
m_overNotShown = true; m_overNotShown = true;
m_jniThreadRef = JNIThread.getRetained( m_rowid, true ); // getRetained() can in threory fail to get the lock and so will
// return null. Let m_jniThreadRef stay null in that case; doResume()
// will finish() in that case.
m_jniThreadRef = JNIThread.getRetained( m_activity, m_rowid, true );
if ( null != m_jniThreadRef ) {
// see http://stackoverflow.com/questions/680180/where-to-stop- \
// destroy-threads-in-android-service-class
m_jniThreadRef.setDaemonOnce( true );
m_jniThreadRef.startOnce();
// see http://stackoverflow.com/questions/680180/where-to-stop- \ NFCUtils.register( m_activity, this ); // Don't seem to need to unregister...
// destroy-threads-in-android-service-class
m_jniThreadRef.setDaemonOnce( true ); // firing
m_jniThreadRef.startOnce();
NFCUtils.register( m_activity, this ); // Don't seem to need to unregister... setBackgroundColor();
setKeepScreenOn();
setBackgroundColor(); }
setKeepScreenOn();
} // init } // init
@Override @Override
@ -628,7 +610,7 @@ public class BoardDelegate extends DelegateBase
@Override @Override
protected void onStop() protected void onStop()
{ {
if ( isFinishing() ) { if ( isFinishing() && null != m_jniThreadRef ) {
m_jniThreadRef.release(); m_jniThreadRef.release();
m_jniThreadRef = null; m_jniThreadRef = null;
} }
@ -1217,7 +1199,9 @@ public class BoardDelegate extends DelegateBase
RequestCode.BT_INVITE_RESULT ); RequestCode.BT_INVITE_RESULT );
break; break;
case SMS: case SMS:
Perms23.tryGetPerms( this, Perm.SEND_SMS, R.string.sms_invite_rationale, Perms23.tryGetPerms( this, new Perm[] { Perm.SEND_SMS,
Perm.RECEIVE_SMS },
R.string.sms_invite_rationale,
Action.INVITE_SMS, m_mySIS.nMissing, info ); Action.INVITE_SMS, m_mySIS.nMissing, info );
break; break;
case RELAY: case RELAY:
@ -1468,6 +1452,7 @@ public class BoardDelegate extends DelegateBase
////////////////////////////////////////////////// //////////////////////////////////////////////////
// ConnStatusHandler.ConnStatusCBacks // ConnStatusHandler.ConnStatusCBacks
////////////////////////////////////////////////// //////////////////////////////////////////////////
@Override
public void invalidateParent() public void invalidateParent()
{ {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@ -1478,22 +1463,13 @@ public class BoardDelegate extends DelegateBase
}); });
} }
@Override
public void onStatusClicked() public void onStatusClicked()
{ {
final String msg = ConnStatusHandler.getStatusText( m_activity, m_connTypes ); onStatusClicked( m_jniGamePtr );
post( new Runnable() {
@Override
public void run() {
if ( null == msg ) {
askNoAddrsDelete();
} else {
showDialogFragment( DlgID.DLG_CONNSTAT, null,
R.string.info_title, msg );
}
}
} );
} }
@Override
public Handler getHandler() public Handler getHandler()
{ {
return m_handler; return m_handler;
@ -1506,15 +1482,6 @@ public class BoardDelegate extends DelegateBase
finish(); finish();
} }
private void askNoAddrsDelete()
{
makeConfirmThenBuilder( R.string.connstat_net_noaddr,
Action.DELETE_AND_EXIT )
.setPosButton( R.string.list_item_delete )
.setNegButton( R.string.button_close_game )
.show();
}
private void askDropRelay() private void askDropRelay()
{ {
String msg = getString( R.string.confirm_drop_relay ); String msg = getString( R.string.confirm_drop_relay );
@ -1853,46 +1820,52 @@ public class BoardDelegate extends DelegateBase
{ {
int resid = 0; int resid = 0;
boolean asToast = false; boolean asToast = false;
String msg = null;
switch( code ) { switch( code ) {
case UtilCtxt.ERR_TILES_NOT_IN_LINE: case ERR_TILES_NOT_IN_LINE:
resid = R.string.str_tiles_not_in_line; resid = R.string.str_tiles_not_in_line;
break; break;
case UtilCtxt.ERR_NO_EMPTIES_IN_TURN: case ERR_NO_EMPTIES_IN_TURN:
resid = R.string.str_no_empties_in_turn; resid = R.string.str_no_empties_in_turn;
break; break;
case UtilCtxt.ERR_TWO_TILES_FIRST_MOVE: case ERR_TWO_TILES_FIRST_MOVE:
resid = R.string.str_two_tiles_first_move; resid = R.string.str_two_tiles_first_move;
break; break;
case UtilCtxt.ERR_TILES_MUST_CONTACT: case ERR_TILES_MUST_CONTACT:
resid = R.string.str_tiles_must_contact; resid = R.string.str_tiles_must_contact;
break; break;
case UtilCtxt.ERR_NOT_YOUR_TURN: case ERR_NOT_YOUR_TURN:
resid = R.string.str_not_your_turn; resid = R.string.str_not_your_turn;
break; break;
case UtilCtxt.ERR_NO_PEEK_ROBOT_TILES: case ERR_NO_PEEK_ROBOT_TILES:
resid = R.string.str_no_peek_robot_tiles; resid = R.string.str_no_peek_robot_tiles;
break; break;
case UtilCtxt.ERR_NO_EMPTY_TRADE: case ERR_NO_EMPTY_TRADE:
// This should not be possible as the button's // This should not be possible as the button's
// disabled when no tiles selected. // disabled when no tiles selected.
Assert.fail(); Assert.fail();
break; break;
case UtilCtxt.ERR_TOO_FEW_TILES_LEFT_TO_TRADE: case ERR_TOO_MANY_TRADE:
int nLeft = XwJNI.server_countTilesInPool( m_jniGamePtr );
msg = getQuantityString( R.plurals.too_many_trade_fmt,
nLeft, nLeft );
break;
case ERR_TOO_FEW_TILES_LEFT_TO_TRADE:
resid = R.string.str_too_few_tiles_left_to_trade; resid = R.string.str_too_few_tiles_left_to_trade;
break; break;
case UtilCtxt.ERR_CANT_UNDO_TILEASSIGN: case ERR_CANT_UNDO_TILEASSIGN:
resid = R.string.str_cant_undo_tileassign; resid = R.string.str_cant_undo_tileassign;
break; break;
case UtilCtxt.ERR_CANT_HINT_WHILE_DISABLED: case ERR_CANT_HINT_WHILE_DISABLED:
resid = R.string.str_cant_hint_while_disabled; resid = R.string.str_cant_hint_while_disabled;
break; break;
case UtilCtxt.ERR_NO_PEEK_REMOTE_TILES: case ERR_NO_PEEK_REMOTE_TILES:
resid = R.string.str_no_peek_remote_tiles; resid = R.string.str_no_peek_remote_tiles;
break; break;
case UtilCtxt.ERR_REG_UNEXPECTED_USER: case ERR_REG_UNEXPECTED_USER:
resid = R.string.str_reg_unexpected_user; resid = R.string.str_reg_unexpected_user;
break; break;
case UtilCtxt.ERR_SERVER_DICT_WINS: case ERR_SERVER_DICT_WINS:
resid = R.string.str_server_dict_wins; resid = R.string.str_server_dict_wins;
break; break;
case ERR_REG_SERVER_SANS_REMOTE: case ERR_REG_SERVER_SANS_REMOTE:
@ -1904,17 +1877,21 @@ public class BoardDelegate extends DelegateBase
break; break;
} }
if ( resid != 0 ) { if ( null == msg && resid != 0 ) {
msg = getString( resid );
}
if ( null != msg ) {
if ( asToast ) { if ( asToast ) {
final int residf = resid; final String msgf = msg;
runOnUiThread( new Runnable() { runOnUiThread( new Runnable() {
@Override @Override
public void run() { public void run() {
showToast( residf ); showToast( msgf );
} }
} ); } );
} else { } else {
nonBlockingDialog( DlgID.DLG_OKONLY, getString( resid ) ); nonBlockingDialog( DlgID.DLG_OKONLY, msg );
} }
} }
} // userError } // userError
@ -2066,9 +2043,9 @@ public class BoardDelegate extends DelegateBase
private void doResume( boolean isStart ) private void doResume( boolean isStart )
{ {
boolean success = true; boolean success = null != m_jniThreadRef;
boolean firstStart = null == m_handler; boolean firstStart = null == m_handler;
if ( firstStart ) { if ( success && firstStart ) {
m_handler = new Handler(); m_handler = new Handler();
success = m_jniThreadRef.configure( m_activity, m_view, m_utils, this, success = m_jniThreadRef.configure( m_activity, m_view, m_utils, this,
@ -2631,9 +2608,7 @@ public class BoardDelegate extends DelegateBase
private boolean inArchiveGroup() private boolean inArchiveGroup()
{ {
String archiveName = LocUtils long archiveGroup = DBUtils.getArchiveGroup( m_activity );
.getString( m_activity, R.string.group_name_archive );
long archiveGroup = DBUtils.getGroup( m_activity, archiveName );
long curGroup = DBUtils.getGroupForGame( m_activity, m_rowid ); long curGroup = DBUtils.getGroupForGame( m_activity, m_rowid );
return curGroup == archiveGroup; return curGroup == archiveGroup;
} }
@ -2770,18 +2745,21 @@ public class BoardDelegate extends DelegateBase
GamePtr gamePtr = null; GamePtr gamePtr = null;
GameSummary summary = null; GameSummary summary = null;
CurGameInfo gi = null; CurGameInfo gi = null;
JNIThread thread = JNIThread.getRetained( rowID ); JNIThread thread = JNIThread.getRetained( activity, rowID );
if ( null != thread ) { if ( null != thread ) {
gamePtr = thread.getGamePtr().retain(); gamePtr = thread.getGamePtr().retain();
summary = thread.getSummary(); summary = thread.getSummary();
gi = thread.getGI(); gi = thread.getGI();
} else { } else {
GameLock lock = new GameLock( rowID, false ); try ( GameLock lock = GameLock.getFor( rowID ).tryLockRO() ) {
if ( lock.tryLock() ) { if ( null != lock ) {
summary = DBUtils.getSummary( activity, lock ); summary = DBUtils.getSummary( activity, lock );
gi = new CurGameInfo( activity ); gi = new CurGameInfo( activity );
gamePtr = GameUtils.loadMakeGame( activity, gi, lock ); gamePtr = GameUtils.loadMakeGame( activity, gi, lock );
lock.unlock(); } else {
DbgUtils.toastNoLock( TAG, activity,
"setupRematchFor(%d)", rowID );
}
} }
} }

View file

@ -31,7 +31,6 @@ import android.util.AttributeSet;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import junit.framework.Assert;
import org.eehouse.android.xw4.jni.BoardDims; import org.eehouse.android.xw4.jni.BoardDims;
import org.eehouse.android.xw4.jni.BoardHandler; import org.eehouse.android.xw4.jni.BoardHandler;
@ -46,7 +45,6 @@ public class BoardView extends View implements BoardHandler, SyncedDraw {
private static final float MIN_FONT_DIPS = 10.0f; private static final float MIN_FONT_DIPS = 10.0f;
private static final int MULTI_INACTIVE = -1; private static final int MULTI_INACTIVE = -1;
private static final int VERSION_CODES_N = 24; // until we're building on SDK 24...
private static boolean s_isFirstDraw; private static boolean s_isFirstDraw;
private static int s_curGameID; private static int s_curGameID;
@ -205,7 +203,7 @@ public class BoardView extends View implements BoardHandler, SyncedDraw {
synchronized( this ) { synchronized( this ) {
if ( layoutBoardOnce() && m_measuredFromDims ) { if ( layoutBoardOnce() && m_measuredFromDims ) {
Bitmap bitmap = s_bitmap; Bitmap bitmap = s_bitmap;
if ( Build.VERSION.SDK_INT >= VERSION_CODES_N ) { if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ) {
bitmap = Bitmap.createBitmap(bitmap); bitmap = Bitmap.createBitmap(bitmap);
} }
canvas.drawBitmap( bitmap, 0, 0, new Paint() ); canvas.drawBitmap( bitmap, 0, 0, new Paint() );
@ -224,7 +222,7 @@ public class BoardView extends View implements BoardHandler, SyncedDraw {
final int height = getHeight(); final int height = getHeight();
boolean layoutDone = width == m_layoutWidth && height == m_layoutHeight; boolean layoutDone = width == m_layoutWidth && height == m_layoutHeight;
if ( layoutDone ) { if ( layoutDone ) {
Log.d( TAG, "layoutBoardOnce(): layoutDone true" ); // Log.d( TAG, "layoutBoardOnce(): layoutDone true" );
} else if ( null == m_gi ) { } else if ( null == m_gi ) {
// nothing to do either // nothing to do either
Log.d( TAG, "layoutBoardOnce(): no m_gi" ); Log.d( TAG, "layoutBoardOnce(): no m_gi" );
@ -283,7 +281,7 @@ public class BoardView extends View implements BoardHandler, SyncedDraw {
m_layoutHeight = height; m_layoutHeight = height;
layoutDone = true; layoutDone = true;
} }
Log.d( TAG, "layoutBoardOnce()=>%b", layoutDone ); // Log.d( TAG, "layoutBoardOnce()=>%b", layoutDone );
return layoutDone; return layoutDone;
} // layoutBoardOnce } // layoutBoardOnce

View file

@ -0,0 +1,72 @@
/* -*- compile-command: "find-and-gradle.sh insXw4Deb"; -*- */
/*
* Copyright 2010 - 2014 by Eric House (xwords@eehouse.org). All
* rights reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This program 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
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package org.eehouse.android.xw4;
import android.os.Build;
import android.content.Context;
import java.util.HashSet;
import java.util.Set;
import android.app.NotificationChannel;
import android.app.NotificationManager;
public class Channels {
enum ID {
FOREGROUND(R.string.foreground_channel_expl,
NotificationManager.IMPORTANCE_LOW),
GAME_EVENT(R.string.gameevent_channel_expl,
NotificationManager.IMPORTANCE_LOW);
private int mExpl;
private int mImportance;
private ID( int expl, int imp )
{
mExpl = expl;
mImportance = imp;
}
public int getDesc() { return mExpl; }
private int getImportance() { return mImportance; }
}
private static Set<ID> sChannelsMade = new HashSet<>();
public static String getChannelID( Context context, ID id )
{
final String name = id.toString();
if ( ! sChannelsMade.contains( id ) ) {
sChannelsMade.add( id );
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ) {
NotificationManager notMgr = (NotificationManager)
context.getSystemService( Context.NOTIFICATION_SERVICE );
NotificationChannel channel = notMgr.getNotificationChannel( name );
if ( channel == null ) {
String channelDescription = context.getString( id.getDesc() );
channel = new NotificationChannel( name, channelDescription,
id.getImportance() );
channel.enableVibration( true );
notMgr.createNotificationChannel( channel );
}
}
}
return name;
}
}

View file

@ -40,7 +40,6 @@ import android.widget.TextView;
import java.text.DateFormat; import java.text.DateFormat;
import android.text.format.DateUtils; import android.text.format.DateUtils;
import junit.framework.Assert;
import org.eehouse.android.xw4.DlgDelegate.Action; import org.eehouse.android.xw4.DlgDelegate.Action;
import org.eehouse.android.xw4.jni.JNIThread; import org.eehouse.android.xw4.jni.JNIThread;
@ -131,7 +130,7 @@ public class ChatDelegate extends DelegateBase {
protected void onResume() protected void onResume()
{ {
super.onResume(); super.onResume();
m_jniThreadRef = JNIThread.getRetained( m_rowid ); m_jniThreadRef = JNIThread.getRetained( m_activity, m_rowid );
if ( null == m_jniThreadRef ) { if ( null == m_jniThreadRef ) {
Log.w( TAG, "onResume(): m_jniThreadRef null; exiting" ); Log.w( TAG, "onResume(): m_jniThreadRef null; exiting" );
finish(); finish();

View file

@ -22,7 +22,6 @@ package org.eehouse.android.xw4;
import android.content.Context; import android.content.Context;
import junit.framework.Assert;
import org.eehouse.android.xw4.jni.CommsAddrRec; import org.eehouse.android.xw4.jni.CommsAddrRec;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType; import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType;

View file

@ -32,10 +32,10 @@ import android.provider.Settings;
import android.text.format.DateUtils; import android.text.format.DateUtils;
import android.text.format.Time; import android.text.format.Time;
import junit.framework.Assert;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType; import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnTypeSet; import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnTypeSet;
import org.eehouse.android.xw4.jni.CommsAddrRec;
import org.eehouse.android.xw4.jni.XwJNI; import org.eehouse.android.xw4.jni.XwJNI;
import org.eehouse.android.xw4.loc.LocUtils; import org.eehouse.android.xw4.loc.LocUtils;
@ -164,7 +164,9 @@ public class ConnStatusHandler {
return s_downOnMe && s_rect.contains( xx, yy ); return s_downOnMe && s_rect.contains( xx, yy );
} }
public static String getStatusText( Context context, CommsConnTypeSet connTypes ) public static String getStatusText( Context context,
CommsConnTypeSet connTypes,
CommsAddrRec addr )
{ {
String msg; String msg;
if ( null == connTypes || 0 == connTypes.size() ) { if ( null == connTypes || 0 == connTypes.size() ) {
@ -177,9 +179,12 @@ public class ConnStatusHandler {
R.string.connstat_net_fmt, R.string.connstat_net_fmt,
connTypes.toString( context, true ))); connTypes.toString( context, true )));
for ( CommsConnType typ : connTypes.getTypes() ) { for ( CommsConnType typ : connTypes.getTypes() ) {
String did = addDebugInfo( context, typ ); sb.append( String.format( "\n\n*** %s ", typ.longName( context ) ) );
sb.append( String.format( "\n\n*** %s %s***\n", String did = addDebugInfo( context, addr, typ );
typ.longName( context ), did ) ); if ( null != did ) {
sb.append( did ).append( " " );
}
sb.append( "***\n" );
// For sends we list failures too. // For sends we list failures too.
SuccessRecord record = recordFor( context, typ, false ); SuccessRecord record = recordFor( context, typ, false );
@ -533,23 +538,39 @@ public class ConnStatusHandler {
return result; return result;
} }
private static String addDebugInfo( Context context, CommsConnType typ ) private static String addDebugInfo( Context context, CommsAddrRec addr,
CommsConnType typ )
{ {
String result = ""; String result = null;
if ( BuildConfig.DEBUG ) { if ( BuildConfig.DEBUG ) {
switch ( typ ) { switch ( typ ) {
case COMMS_CONN_RELAY: case COMMS_CONN_RELAY:
result = String.format( "(DevID: %d; host: %s) ", result = String.format( "DevID: %d; host: %s",
DevID.getRelayDevIDInt(context), DevID.getRelayDevIDInt(context),
XWPrefs.getDefaultRelayHost(context) ); XWPrefs.getDefaultRelayHost(context) );
break; break;
case COMMS_CONN_P2P: case COMMS_CONN_P2P:
result = WiDirService.formatNetStateInfo(); result = WiDirService.formatNetStateInfo();
break; break;
case COMMS_CONN_BT:
if ( null != addr ) {
result = addr.bt_hostName;
}
break;
case COMMS_CONN_SMS:
if ( null != addr ) {
result = addr.sms_phone;
}
break;
default: default:
break; break;
} }
} }
if ( null != result ) {
result = "(" + result + ")";
}
return result; return result;
} }
} }

View file

@ -30,7 +30,6 @@ import android.widget.LinearLayout;
import java.util.Map; import java.util.Map;
import junit.framework.Assert;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType; import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnTypeSet; import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnTypeSet;

View file

@ -34,7 +34,6 @@ import org.eehouse.android.xw4.loc.LocUtils;
import java.io.Serializable; import java.io.Serializable;
import junit.framework.Assert;
public class DBAlert extends XWDialogFragment { public class DBAlert extends XWDialogFragment {
private static final String TAG = DBAlert.class.getSimpleName(); private static final String TAG = DBAlert.class.getSimpleName();

View file

@ -33,7 +33,6 @@ import android.graphics.BitmapFactory;
import android.os.Environment; import android.os.Environment;
import android.text.TextUtils; import android.text.TextUtils;
import junit.framework.Assert;
import org.eehouse.android.xw4.DictUtils.DictLoc; import org.eehouse.android.xw4.DictUtils.DictLoc;
import org.eehouse.android.xw4.DlgDelegate.DlgClickNotify.InviteMeans; import org.eehouse.android.xw4.DlgDelegate.DlgClickNotify.InviteMeans;
@ -367,22 +366,27 @@ public class DBUtils {
public static void addRematchInfo( Context context, long rowid, String btAddr, public static void addRematchInfo( Context context, long rowid, String btAddr,
String phone, String relayID, String p2pAddr ) String phone, String relayID, String p2pAddr )
{ {
GameLock lock = new GameLock( rowid, true ).lock(); try ( GameLock lock = GameLock.getFor( rowid ).tryLock() ) {
GameSummary summary = getSummary( context, lock ); Assert.assertNotNull( lock );
if ( null != btAddr ) { if ( null != lock ) {
summary.putStringExtra( GameSummary.EXTRA_REMATCH_BTADDR, btAddr ); GameSummary summary = getSummary( context, lock );
if ( null != btAddr ) {
summary.putStringExtra( GameSummary.EXTRA_REMATCH_BTADDR, btAddr );
}
if ( null != phone ) {
summary.putStringExtra( GameSummary.EXTRA_REMATCH_PHONE, phone );
}
if ( null != relayID ) {
summary.putStringExtra( GameSummary.EXTRA_REMATCH_RELAY, relayID );
}
if ( null != p2pAddr ) {
summary.putStringExtra( GameSummary.EXTRA_REMATCH_P2P, p2pAddr );
}
saveSummary( context, lock, summary );
} else {
Log.e( TAG, "addRematchInfo(%d): unable to lock game" );
}
} }
if ( null != phone ) {
summary.putStringExtra( GameSummary.EXTRA_REMATCH_PHONE, phone );
}
if ( null != relayID ) {
summary.putStringExtra( GameSummary.EXTRA_REMATCH_RELAY, relayID );
}
if ( null != p2pAddr ) {
summary.putStringExtra( GameSummary.EXTRA_REMATCH_P2P, p2pAddr );
}
saveSummary( context, lock, summary );
lock.unlock();
} }
public static int countGamesUsingLang( Context context, int lang ) public static int countGamesUsingLang( Context context, int lang )
@ -1053,7 +1057,8 @@ public class DBUtils {
setCached( rowid, null ); // force reread setCached( rowid, null ); // force reread
lock = new GameLock( rowid, true ).lock(); lock = GameLock.getFor( rowid ).tryLock();
Assert.assertNotNull( lock );
notifyListeners( rowid, GameChangeType.GAME_CREATED ); notifyListeners( rowid, GameChangeType.GAME_CREATED );
} }
@ -1114,14 +1119,14 @@ public class DBUtils {
public static void deleteGame( Context context, long rowid ) public static void deleteGame( Context context, long rowid )
{ {
GameLock lock = new GameLock( rowid, true ).lock( 300 ); try ( GameLock lock = GameLock.getFor( rowid ).lock( 300 ) ) {
if ( null != lock ) { if ( null != lock ) {
deleteGame( context, lock ); deleteGame( context, lock );
lock.unlock(); } else {
} else { Log.e( TAG, "deleteGame: unable to lock rowid %d", rowid );
Log.e( TAG, "deleteGame: unable to lock rowid %d", rowid ); if ( BuildConfig.DEBUG ) {
if ( BuildConfig.DEBUG ) { Assert.fail();
Assert.fail(); }
} }
} }
} }
@ -1786,6 +1791,14 @@ public class DBUtils {
invalGroupsCache(); invalGroupsCache();
} }
public static long getArchiveGroup( Context context )
{
String archiveName = LocUtils
.getString( context, R.string.group_name_archive );
long archiveGroup = getGroup( context, archiveName );
return archiveGroup;
}
// Change group id of a game // Change group id of a game
public static void moveGame( Context context, long rowid, long groupID ) public static void moveGame( Context context, long rowid, long groupID )
{ {
@ -2395,7 +2408,7 @@ public class DBUtils {
public static void setIntFor( Context context, String key, int value ) public static void setIntFor( Context context, String key, int value )
{ {
// Log.df( "DBUtils.setIntFor(key=%s, val=%d)", key, value ); // Log.d( TAG, "DBUtils.setIntFor(key=%s, val=%d)", key, value );
String asStr = String.format( "%d", value ); String asStr = String.format( "%d", value );
setStringFor( context, key, asStr ); setStringFor( context, key, asStr );
} }
@ -2406,7 +2419,7 @@ public class DBUtils {
if ( null != asStr ) { if ( null != asStr ) {
dflt = Integer.parseInt( asStr ); dflt = Integer.parseInt( asStr );
} }
// Log.df( "DBUtils.getIntFor(key=%s)=>%d", key, dflt ); // Log.d( TAG, "DBUtils.getIntFor(key=%s)=>%d", key, dflt );
return dflt; return dflt;
} }

View file

@ -25,7 +25,6 @@ import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.database.DatabaseUtils; import android.database.DatabaseUtils;
import android.os.Bundle; import android.os.Bundle;
import android.os.Looper;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.format.Time; import android.text.format.Time;
@ -34,7 +33,6 @@ import java.util.Formatter;
import java.util.Iterator; import java.util.Iterator;
import java.util.Set; import java.util.Set;
import junit.framework.Assert;
import org.eehouse.android.xw4.loc.LocUtils; import org.eehouse.android.xw4.loc.LocUtils;
@ -101,9 +99,24 @@ public class DbgUtils {
showf( context, LocUtils.getString( context, formatid ), args ); showf( context, LocUtils.getString( context, formatid ), args );
} // showf } // showf
public static void toastNoLock( String tag, Context context, String format,
Object... args )
{
format = "Unable to lock game; " + format;
if ( BuildConfig.DEBUG ) {
showf( context, format, args );
}
Log.w( tag, format, args );
}
public static void assertOnUIThread() public static void assertOnUIThread()
{ {
Assert.assertTrue( Looper.getMainLooper().equals(Looper.myLooper()) ); assertOnUIThread( true );
}
public static void assertOnUIThread( boolean isOnThread )
{
Assert.assertTrue( isOnThread == Utils.isOnUIThread() );
} }
public static void printStack( String tag, StackTraceElement[] trace ) public static void printStack( String tag, StackTraceElement[] trace )
@ -123,6 +136,12 @@ public class DbgUtils {
} }
} }
public static void printStack( String tag, Exception ex )
{
String stackTrace = android.util.Log.getStackTraceString(ex);
Log.d( tag, stackTrace );
}
static String extrasToString( Intent intent ) static String extrasToString( Intent intent )
{ {
Bundle bundle = intent.getExtras(); Bundle bundle = intent.getExtras();

View file

@ -22,12 +22,15 @@ package org.eehouse.android.xw4;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.Dialog; import android.app.Dialog;
import android.support.v4.app.DialogFragment; import android.content.Context;
import android.content.DialogInterface.OnCancelListener; import android.content.DialogInterface.OnCancelListener;
import android.content.DialogInterface.OnClickListener;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.graphics.Point; import android.graphics.Point;
import android.graphics.Rect; import android.graphics.Rect;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.view.ContextMenu.ContextMenuInfo; import android.view.ContextMenu.ContextMenuInfo;
import android.view.ContextMenu; import android.view.ContextMenu;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -40,8 +43,6 @@ import android.widget.CheckBox;
import android.widget.EditText; import android.widget.EditText;
import android.widget.TextView; import android.widget.TextView;
import junit.framework.Assert;
import org.eehouse.android.xw4.DlgDelegate.Action; import org.eehouse.android.xw4.DlgDelegate.Action;
import org.eehouse.android.xw4.DlgDelegate.ActionPair; import org.eehouse.android.xw4.DlgDelegate.ActionPair;
import org.eehouse.android.xw4.DlgDelegate.ConfirmThenBuilder; import org.eehouse.android.xw4.DlgDelegate.ConfirmThenBuilder;
@ -49,6 +50,12 @@ import org.eehouse.android.xw4.DlgDelegate.DlgClickNotify;
import org.eehouse.android.xw4.DlgDelegate.NotAgainBuilder; import org.eehouse.android.xw4.DlgDelegate.NotAgainBuilder;
import org.eehouse.android.xw4.DlgDelegate.OkOnlyBuilder; import org.eehouse.android.xw4.DlgDelegate.OkOnlyBuilder;
import org.eehouse.android.xw4.MultiService.MultiEvent; import org.eehouse.android.xw4.MultiService.MultiEvent;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnTypeSet;
import org.eehouse.android.xw4.jni.CommsAddrRec;
import org.eehouse.android.xw4.jni.GameSummary;
import org.eehouse.android.xw4.jni.XwJNI.GamePtr;
import org.eehouse.android.xw4.jni.XwJNI;
import org.eehouse.android.xw4.loc.LocUtils; import org.eehouse.android.xw4.loc.LocUtils;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
@ -144,14 +151,15 @@ public class DelegateBase implements DlgClickNotify,
protected void onResume() protected void onResume()
{ {
m_isVisible = true; m_isVisible = true;
XWService.setListener( this ); XWServiceHelper.setListener( this );
runIfVisible(); runIfVisible();
} }
protected void onPause() protected void onPause()
{ {
m_isVisible = false; m_isVisible = false;
XWService.setListener( null ); XWServiceHelper.setListener( null );
m_dlgDelegate.onPausing();
} }
protected DelegateBase curThis() protected DelegateBase curThis()
@ -415,10 +423,44 @@ public class DelegateBase implements DlgClickNotify,
protected Dialog makeDialog( DBAlert alert, Object[] params ) protected Dialog makeDialog( DBAlert alert, Object[] params )
{ {
Dialog dialog = null;
DlgID dlgID = alert.getDlgID(); DlgID dlgID = alert.getDlgID();
Log.d( TAG, "%s.makeDialog(): not handling %s", getClass().getSimpleName(), switch ( dlgID ) {
dlgID.toString() ); case DLG_CONNSTAT: {
return null; AlertDialog.Builder ab = makeAlertBuilder();
GameSummary summary = (GameSummary)params[0];
int title = (Integer)params[1];
String msg = (String)params[2];
final CommsConnTypeSet conTypes = summary.conTypes;
ab.setTitle( title )
.setMessage( msg )
.setPositiveButton( android.R.string.ok, null );
if ( BuildConfig.DEBUG && null != conTypes
&& (conTypes.contains( CommsConnType.COMMS_CONN_RELAY )
|| conTypes.contains( CommsConnType.COMMS_CONN_P2P )) ) {
OnClickListener lstnr = new OnClickListener() {
public void onClick( DialogInterface dlg,
int whichButton ) {
NetStateCache.reset( m_activity );
if ( conTypes.contains( CommsConnType.COMMS_CONN_RELAY ) ) {
RelayService.reset( getActivity() );
}
if ( conTypes.contains( CommsConnType.COMMS_CONN_P2P ) ) {
WiDirService.reset( getActivity() );
}
}
};
ab.setNegativeButton( R.string.button_reconnect, lstnr );
}
dialog = ab.create();
}
break;
default:
Log.d( TAG, "%s.makeDialog(): not handling %s", getClass().getSimpleName(),
dlgID.toString() );
break;
}
return dialog;
} }
protected void showDialogFragment( final DlgID dlgID, final Object... params ) protected void showDialogFragment( final DlgID dlgID, final Object... params )
@ -594,6 +636,53 @@ public class DelegateBase implements DlgClickNotify,
runIfVisible(); runIfVisible();
} }
public void onStatusClicked( GamePtr gamePtr )
{
Context context = getActivity();
CommsAddrRec[] addrs = XwJNI.comms_getAddrs( gamePtr );
CommsAddrRec addr = null != addrs && 0 < addrs.length ? addrs[0] : null;
final GameSummary summary = GameUtils.getSummary( context, gamePtr.getRowid(), 1 );
if ( null != summary ) {
final String msg = ConnStatusHandler
.getStatusText( context, summary.conTypes, addr );
post( new Runnable() {
@Override
public void run() {
if ( null == msg ) {
askNoAddrsDelete();
} else {
showDialogFragment( DlgID.DLG_CONNSTAT, summary,
R.string.info_title, msg );
}
}
} );
}
}
public void onStatusClicked( long rowid )
{
Log.d( TAG, "onStatusClicked(%d)", rowid );
try ( GameLock lock = GameLock.getFor( rowid ).tryLockRO() ) {
if ( null != lock ) {
GamePtr gamePtr = GameUtils.loadMakeGame( getActivity(), lock );
if ( null != gamePtr ) {
onStatusClicked( gamePtr );
gamePtr.release();
}
}
}
}
protected void askNoAddrsDelete()
{
makeConfirmThenBuilder( R.string.connstat_net_noaddr,
Action.DELETE_AND_EXIT )
.setPosButton( R.string.list_item_delete )
.setNegButton( R.string.button_close_game )
.show();
}
////////////////////////////////////////////////// //////////////////////////////////////////////////
// MultiService.MultiEventListener interface // MultiService.MultiEventListener interface
////////////////////////////////////////////////// //////////////////////////////////////////////////
@ -601,10 +690,6 @@ public class DelegateBase implements DlgClickNotify,
{ {
int fmtId = 0; int fmtId = 0;
switch( event ) { switch( event ) {
case BT_ERR_COUNT:
int count = (Integer)args[0];
Log.i( TAG, "Bluetooth error count: %d", count );
break;
case BAD_PROTO_BT: case BAD_PROTO_BT:
fmtId = R.string.bt_bad_proto_fmt; fmtId = R.string.bt_bad_proto_fmt;
break; break;

View file

@ -37,7 +37,6 @@ import android.widget.SectionIndexer;
import android.widget.Spinner; import android.widget.Spinner;
import android.widget.TextView; import android.widget.TextView;
import junit.framework.Assert;
import org.eehouse.android.xw4.DlgDelegate.Action; import org.eehouse.android.xw4.DlgDelegate.Action;
import org.eehouse.android.xw4.jni.JNIUtilsImpl; import org.eehouse.android.xw4.jni.JNIUtilsImpl;

View file

@ -25,7 +25,6 @@ import android.content.res.Resources;
import android.os.Handler; import android.os.Handler;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import junit.framework.Assert;
import org.eehouse.android.xw4.DictUtils.DictAndLoc; import org.eehouse.android.xw4.DictUtils.DictAndLoc;
import org.eehouse.android.xw4.DictUtils.DictLoc; import org.eehouse.android.xw4.DictUtils.DictLoc;
@ -327,7 +326,7 @@ public class DictLangCache {
public static void inval( final Context context, String name, public static void inval( final Context context, String name,
DictLoc loc, boolean added ) DictLoc loc, boolean added )
{ {
DBUtils.dictsRemoveInfo( context, name ); DBUtils.dictsRemoveInfo( context, DictUtils.removeDictExtn( name ) );
if ( added ) { if ( added ) {
DictAndLoc dal = new DictAndLoc( name, loc ); DictAndLoc dal = new DictAndLoc( name, loc );

View file

@ -24,7 +24,6 @@ import android.content.Context;
import android.content.res.AssetManager; import android.content.res.AssetManager;
import android.os.Environment; import android.os.Environment;
import junit.framework.Assert;
import org.eehouse.android.xw4.jni.JNIUtilsImpl; import org.eehouse.android.xw4.jni.JNIUtilsImpl;
import org.eehouse.android.xw4.jni.XwJNI; import org.eehouse.android.xw4.jni.XwJNI;

View file

@ -45,7 +45,6 @@ import android.widget.ListView;
import android.widget.PopupMenu; import android.widget.PopupMenu;
import android.widget.TextView; import android.widget.TextView;
import junit.framework.Assert;
import org.eehouse.android.xw4.DictUtils.DictAndLoc; import org.eehouse.android.xw4.DictUtils.DictAndLoc;
import org.eehouse.android.xw4.DictUtils.DictLoc; import org.eehouse.android.xw4.DictUtils.DictLoc;
@ -1064,23 +1063,6 @@ public class DictsDelegate extends ListDelegateBase
return items; return items;
} }
// private static Intent mkDownloadIntent( Context context, String dict_url )
// {
// Uri uri = Uri.parse( dict_url );
// Intent intent = new Intent( Intent.ACTION_VIEW, uri );
// intent.setFlags( Intent.FLAG_ACTIVITY_NEW_TASK );
// return intent;
// }
private static Intent mkDownloadIntent( Context context,
int lang, String dict )
{
Assert.fail();
return null;
// String dict_url = Utils.makeDictUri( context, lang, dict );
// return mkDownloadIntent( context, dict_url );
}
public static void downloadForResult( Delegator delegator, RequestCode requestCode, public static void downloadForResult( Delegator delegator, RequestCode requestCode,
int lang ) int lang )
{ {

View file

@ -31,7 +31,6 @@ import android.widget.TextView;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType; import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType;
import junit.framework.Assert;
public class DisablesItem extends LinearLayout { public class DisablesItem extends LinearLayout {
private CommsConnType m_type; private CommsConnType m_type;

View file

@ -34,7 +34,6 @@ import android.view.View;
import java.io.Serializable; import java.io.Serializable;
import junit.framework.Assert;
import org.eehouse.android.xw4.DBUtils.SentInvitesInfo; import org.eehouse.android.xw4.DBUtils.SentInvitesInfo;
@ -327,6 +326,11 @@ public class DlgDelegate {
m_handler = new Handler(); m_handler = new Handler();
} }
void onPausing()
{
stopProgress();
}
private void showOKOnlyDialogThen( String msg, Action action, private void showOKOnlyDialogThen( String msg, Action action,
Object[] params, int titleId ) Object[] params, int titleId )
{ {

View file

@ -31,7 +31,6 @@ import org.eehouse.android.xw4.DlgDelegate.Action;
import org.eehouse.android.xw4.DlgDelegate.ActionPair; import org.eehouse.android.xw4.DlgDelegate.ActionPair;
import org.eehouse.android.xw4.DlgDelegate.DlgClickNotify; import org.eehouse.android.xw4.DlgDelegate.DlgClickNotify;
import junit.framework.Assert;
import org.eehouse.android.xw4.loc.LocUtils; import org.eehouse.android.xw4.loc.LocUtils;

View file

@ -32,7 +32,6 @@ import java.util.List;
import org.eehouse.android.xw4.DlgDelegate.Action; import org.eehouse.android.xw4.DlgDelegate.Action;
import org.eehouse.android.xw4.DlgDelegate.ActionPair; import org.eehouse.android.xw4.DlgDelegate.ActionPair;
import junit.framework.Assert;
public class DlgState implements Parcelable { public class DlgState implements Parcelable {
private static final String TAG = DlgState.class.getSimpleName(); private static final String TAG = DlgState.class.getSimpleName();

View file

@ -34,7 +34,6 @@ import android.widget.LinearLayout;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import junit.framework.Assert;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;

View file

@ -30,7 +30,6 @@ import android.widget.AdapterView;
import android.widget.Button; import android.widget.Button;
import android.widget.Spinner; import android.widget.Spinner;
import junit.framework.Assert;
import org.eehouse.android.xw4.loc.LocUtils; import org.eehouse.android.xw4.loc.LocUtils;

View file

@ -29,7 +29,6 @@ import android.graphics.drawable.Drawable;
import android.os.Handler; import android.os.Handler;
import android.view.View; import android.view.View;
import junit.framework.Assert;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.ArrayList; import java.util.ArrayList;

View file

@ -45,7 +45,6 @@ import android.widget.TextView;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import junit.framework.Assert;
import org.eehouse.android.xw4.DictLangCache.LangsArrayAdapter; import org.eehouse.android.xw4.DictLangCache.LangsArrayAdapter;
import org.eehouse.android.xw4.DlgDelegate.Action; import org.eehouse.android.xw4.DlgDelegate.Action;
@ -484,7 +483,7 @@ public class GameConfigDelegate extends DelegateBase
@Override @Override
protected void onResume() protected void onResume()
{ {
m_jniThread = JNIThread.getRetained( m_rowid ); m_jniThread = JNIThread.getRetained( m_activity, m_rowid );
super.onResume(); super.onResume();
loadGame(); loadGame();
} }
@ -537,11 +536,14 @@ public class GameConfigDelegate extends DelegateBase
if ( null == m_giOrig ) { if ( null == m_giOrig ) {
m_giOrig = new CurGameInfo( m_activity ); m_giOrig = new CurGameInfo( m_activity );
GameLock gameLock; GameLock gameLock = null;
XwJNI.GamePtr gamePtr; XwJNI.GamePtr gamePtr = null;
if ( null == m_jniThread ) { if ( null == m_jniThread ) {
gameLock = new GameLock( m_rowid, false ).lock(); gameLock = GameLock.getFor( m_rowid ).tryLockRO();
gamePtr = GameUtils.loadMakeGame( m_activity, m_giOrig, gameLock ); if ( null != gameLock ) {
gamePtr = GameUtils.loadMakeGame( m_activity, m_giOrig,
gameLock );
}
} else { } else {
gameLock = m_jniThread.getLock(); gameLock = m_jniThread.getLock();
gamePtr = m_jniThread.getGamePtr(); gamePtr = m_jniThread.getGamePtr();
@ -1232,7 +1234,7 @@ public class GameConfigDelegate extends DelegateBase
{ {
if ( !isFinishing() ) { if ( !isFinishing() ) {
GameLock gameLock = m_jniThread == null GameLock gameLock = m_jniThread == null
? new GameLock( m_rowid, true ).lock() ? GameLock.getFor( m_rowid ).tryLock()
: m_jniThread.getLock(); : m_jniThread.getLock();
GameUtils.applyChanges( m_activity, m_gi, m_car, m_disabMap, GameUtils.applyChanges( m_activity, m_gi, m_car, m_disabMap,
gameLock, forceNew ); gameLock, forceNew );

View file

@ -33,7 +33,6 @@ import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import junit.framework.Assert;
import org.eehouse.android.xw4.jni.GameSummary; import org.eehouse.android.xw4.jni.GameSummary;
import org.eehouse.android.xw4.jni.JNIThread; import org.eehouse.android.xw4.jni.JNIThread;

View file

@ -20,179 +20,223 @@
package org.eehouse.android.xw4; package org.eehouse.android.xw4;
import junit.framework.Assert; import java.io.Serializable;
import java.lang.ref.WeakReference;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
import android.support.annotation.NonNull;
// Implements read-locks and write-locks per game. A read lock is // Implements read-locks and write-locks per game. A read lock is
// obtainable when other read locks are granted but not when a // obtainable when other read locks are granted but not when a
// write lock is. Write-locks are exclusive. // write lock is. Write-locks are exclusive.
public class GameLock { public class GameLock implements AutoCloseable {
private static final String TAG = GameLock.class.getSimpleName(); private static final String TAG = GameLock.class.getSimpleName();
private static final boolean DEBUG_LOCKS = false; private static final boolean DEBUG_LOCKS = false;
private static final boolean THROW_ON_LOCKED = true; // private static final long ASSERT_TIME = 2000;
private static final int SLEEP_TIME = 100;
private static final long ASSERT_TIME = 2000;
private static final long THROW_TIME = 1000; private static final long THROW_TIME = 1000;
private long m_rowid; private long m_rowid;
private boolean m_isForWrite; private Stack<Owner> mOwners = new Stack<>();
private int m_lockCount; private boolean mReadOnly;
private Thread m_ownerThread;
private StackTraceElement[] m_lockTrace;
static { private static class Owner {
Assert.assertTrue( THROW_TIME <= ASSERT_TIME ); Thread mThread;
String mTrace;
Owner()
{
mThread = Thread.currentThread();
// mTrace = mThread.getStackTrace();
mTrace = android.util.Log.getStackTraceString(new Exception());
}
@Override
public String toString()
{
return String.format( "Owner: {%s/%s}", mThread, mTrace );
}
}
@Override
public String toString()
{
return String.format("{this: %H; rowid: %d; count: %d; ro: %b}",
this, m_rowid, mOwners.size(), mReadOnly);
} }
public static class GameLockedException extends RuntimeException {} public static class GameLockedException extends RuntimeException {}
private static HashMap<Long, GameLock> private static Map<Long, WeakReference<GameLock>> sLockMap = new HashMap<>();
s_locks = new HashMap<Long,GameLock>(); public static GameLock getFor( long rowid )
public GameLock( long rowid, boolean isForWrite )
{
Assert.assertTrue( DBUtils.ROWID_NOTFOUND != rowid );
m_rowid = rowid;
m_isForWrite = isForWrite;
m_lockCount = 0;
if ( DEBUG_LOCKS ) {
Log.i( TAG, "GameLock(rowid:%d,isForWrite:%b)=>this: %H",
rowid, isForWrite, this );
DbgUtils.printStack( TAG );
}
}
// This could be written to allow multiple read locks. Let's
// see if not doing that causes problems.
public boolean tryLock()
{
return null == tryLockImpl(); // unowned?
}
private GameLock tryLockImpl()
{
GameLock owner = null;
boolean gotIt = false;
synchronized( s_locks ) {
owner = s_locks.get( m_rowid );
if ( null == owner ) { // unowned
Assert.assertTrue( 0 == m_lockCount );
s_locks.put( m_rowid, this );
if ( BuildConfig.DEBUG ) {
m_ownerThread = Thread.currentThread();
}
++m_lockCount;
gotIt = true;
if ( DEBUG_LOCKS ) {
StackTraceElement[] trace
= Thread.currentThread().getStackTrace();
m_lockTrace = new StackTraceElement[trace.length];
System.arraycopy( trace, 0, m_lockTrace, 0, trace.length );
}
} else if ( this == owner && ! m_isForWrite ) {
if ( DEBUG_LOCKS ) {
Log.i( TAG, "tryLock(): incrementing lock count" );
}
Assert.assertTrue( 0 == m_lockCount );
++m_lockCount;
gotIt = true;
owner = null;
} else if ( DEBUG_LOCKS ) {
Log.i( TAG, "tryLock(): rowid %d already held by lock %H",
m_rowid, owner );
}
}
if ( DEBUG_LOCKS ) {
Log.i( TAG, "tryLock %H (rowid=%d) => %b", this, m_rowid, gotIt );
}
return owner;
}
// Wait forever (but may assert if too long)
public GameLock lock()
{
return this.lock( 0 );
}
// Version that's allowed to return null -- if maxMillis > 0
public GameLock lock( long maxMillis )
{ {
GameLock result = null; GameLock result = null;
Assert.assertTrue( maxMillis <= ASSERT_TIME ); synchronized ( sLockMap ) {
Assert.assertTrue( maxMillis <= THROW_TIME ); if ( sLockMap.containsKey( rowid ) ) {
long sleptTime = 0; result = sLockMap.get( rowid ).get();
}
if ( null == result ) {
result = new GameLock( rowid );
sLockMap.put( rowid, new WeakReference(result) );
}
}
return result;
}
private GameLock( long rowid ) { m_rowid = rowid; }
// We grant a lock IFF:
// * Count is 0
// OR
// * existing locks are ReadOnly and this request is readOnly
// OR
// * the requesting thread already holds the lock
private GameLock tryLockImpl( boolean readOnly )
{
GameLock result = null;
synchronized ( mOwners ) {
if ( DEBUG_LOCKS ) {
Log.d( TAG, "%s.tryLockImpl(ro=%b)", this, readOnly );
}
// Thread thisThread = Thread.currentThread();
boolean grant = false;
if ( mOwners.empty() ) {
grant = true;
} else if ( mReadOnly && readOnly ) {
grant = true;
// } else if ( thisThread == mOwnerThread ) {
// grant = true;
}
if ( grant ) {
mOwners.push( new Owner() );
mReadOnly = readOnly;
result = this;
}
if ( DEBUG_LOCKS ) {
Log.d( TAG, "%s.tryLockImpl(ro=%b) => %s", this, readOnly, result );
if ( null == result ) {
Log.d( TAG, "Unable to lock; cur owner: %s; would-be owner: %s",
mOwners.peek(), new Owner() );
}
}
}
return result;
}
// // This could be written to allow multiple read locks. Let's
// // see if not doing that causes problems.
public GameLock tryLock()
{
return tryLockImpl( false );
}
public GameLock tryLockRO()
{
return tryLockImpl( true );
}
private GameLock lockImpl( long timeoutMS, boolean readOnly ) throws InterruptedException
{
long startMS = System.currentTimeMillis();
long endMS = startMS + timeoutMS;
synchronized ( mOwners ) {
while ( null == tryLockImpl( readOnly ) ) {
long now = System.currentTimeMillis();
if ( now >= endMS ) {
throw new GameLockedException();
}
mOwners.wait( endMS - now );
}
}
if ( DEBUG_LOCKS ) { if ( DEBUG_LOCKS ) {
Log.i( TAG, "lock %H (rowid:%d, maxMillis=%d)", this, m_rowid, long tookMS = System.currentTimeMillis() - startMS;
maxMillis ); Log.d( TAG, "%s.lockImpl() returning after %d ms", this, tookMS );
} }
return this;
}
for ( ; ; ) { @NonNull
GameLock curOwner = tryLockImpl(); public GameLock lock() throws InterruptedException
if ( null == curOwner ) { {
result = this; if ( BuildConfig.DEBUG ) {
break; DbgUtils.assertOnUIThread( false );
}
if ( DEBUG_LOCKS ) {
Log.i( TAG, "lock() %H failed; sleeping", this );
if ( 0 == sleptTime || sleptTime + SLEEP_TIME >= ASSERT_TIME ) {
Log.i( TAG, "lock %H seeking stack:", curOwner );
DbgUtils.printStack( TAG, curOwner.m_lockTrace );
Log.i( TAG, "lock %H seeking stack:", this );
DbgUtils.printStack( TAG );
}
}
try {
Thread.sleep( SLEEP_TIME ); // milliseconds
sleptTime += SLEEP_TIME;
} catch( InterruptedException ie ) {
Log.ex( TAG, ie );
break;
}
if ( DEBUG_LOCKS ) {
Log.i( TAG, "lock() %H awake; "
+ "sleptTime now %d millis", this, sleptTime );
}
if ( 0 < maxMillis && sleptTime >= maxMillis ) {
break;
} else if ( THROW_ON_LOCKED && sleptTime >= THROW_TIME ) {
throw new GameLockedException();
} else if ( sleptTime >= ASSERT_TIME ) {
if ( DEBUG_LOCKS ) {
Log.i( TAG, "lock %H overlocked", this );
}
Assert.fail();
}
} }
// DbgUtils.logf( "GameLock.lock(%s) done", m_path ); GameLock result = lockImpl( Long.MAX_VALUE, false );
Assert.assertNotNull( result );
return result; return result;
} }
// @NonNull
// public GameLock lockRO() throws InterruptedException
// {
// if ( BuildConfig.DEBUG ) {
// DbgUtils.assertOnUIThread( false );
// }
// GameLock result = lockImpl( Long.MAX_VALUE, true );
// Assert.assertNotNull( result );
// return result;
// }
// Version that's allowed to return null -- if maxMillis > 0
public GameLock lock( long maxMillis ) throws GameLockedException
{
Assert.assertTrue( maxMillis <= THROW_TIME );
GameLock result = null;
try {
result = lockImpl( maxMillis, false );
} catch (InterruptedException ex) {
Log.d( TAG, "lock(): got %s", ex.getMessage() );
}
if ( DEBUG_LOCKS ) {
Log.d( TAG, "%s.lock(%d) => %s", this, maxMillis, result );
}
return result;
}
public GameLock lockRO( long maxMillis )
{
Assert.assertTrue( maxMillis <= THROW_TIME );
GameLock lock = null;
try {
lock = lockImpl( maxMillis, true );
} catch ( InterruptedException ex ) {
}
return lock;
}
public void unlock() public void unlock()
{ {
// DbgUtils.logf( "GameLock.unlock(%s)", m_path ); synchronized ( mOwners ) {
synchronized( s_locks ) {
Assert.assertTrue( this == s_locks.get(m_rowid) );
// Need to get this working before can switch to using ReentrantLock
// if ( BuildConfig.DEBUG ) {
// Assert.assertTrue( Thread.currentThread().equals(m_ownerThread) );
// }
if ( 1 == m_lockCount ) {
s_locks.remove( m_rowid );
} else {
Assert.assertTrue( !m_isForWrite );
}
--m_lockCount;
if ( DEBUG_LOCKS ) { if ( DEBUG_LOCKS ) {
Log.i( TAG, "unlock: this: %H (rowid:%d) unlocked", this, m_rowid ); Log.d( TAG, "%s.unlock()", this );
}
Thread oldThread = mOwners.pop().mThread;
// It's ok for different threads to hold the same RO lock
if ( !mReadOnly && oldThread != Thread.currentThread() ) {
Log.e( TAG, "unlock(): unequal threads: %s => %s", oldThread,
Thread.currentThread() );
Assert.fail();
}
if ( mOwners.empty() ) {
mOwners.notifyAll();
}
if ( DEBUG_LOCKS ) {
Log.d( TAG, "%s.unlock() DONE", this );
} }
} }
} }
@Override
public void close()
{
unlock();
}
public long getRowid() public long getRowid()
{ {
return m_rowid; return m_rowid;
@ -201,9 +245,9 @@ public class GameLock {
// used only for asserts // used only for asserts
public boolean canWrite() public boolean canWrite()
{ {
boolean result = m_isForWrite && 1 == m_lockCount; boolean result = !mReadOnly; // && 1 == mLockCount[0];
if ( !result ) { if ( !result ) {
Log.w( TAG, "canWrite(): %H, returning false", this ); Log.w( TAG, "%s.canWrite(): => false", this );
} }
return result; return result;
} }

View file

@ -31,7 +31,6 @@ import android.text.Html;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.Display; import android.view.Display;
import junit.framework.Assert;
import org.eehouse.android.xw4.jni.CommonPrefs; import org.eehouse.android.xw4.jni.CommonPrefs;
import org.eehouse.android.xw4.jni.CommsAddrRec; import org.eehouse.android.xw4.jni.CommsAddrRec;
@ -93,9 +92,12 @@ public class GameUtils {
public static byte[] savedGame( Context context, long rowid ) public static byte[] savedGame( Context context, long rowid )
{ {
GameLock lock = new GameLock( rowid, false ).lock(); byte[] result = null;
byte[] result = savedGame( context, lock ); try (GameLock lock = GameLock.getFor( rowid ).tryLockRO() ) {
lock.unlock(); if ( null != lock ) {
result = savedGame( context, lock );
}
}
if ( null == result ) { if ( null == result ) {
throw new NoSuchGameException(); throw new NoSuchGameException();
@ -145,7 +147,7 @@ public class GameUtils {
if ( null == lockDest ) { if ( null == lockDest ) {
long groupID = DBUtils.getGroupForGame( context, lockSrc.getRowid() ); long groupID = DBUtils.getGroupForGame( context, lockSrc.getRowid() );
long rowid = saveNewGame( context, gamePtr, gi, groupID ); long rowid = saveNewGame( context, gamePtr, gi, groupID );
lockDest = new GameLock( rowid, true ).lock(); lockDest = GameLock.getFor( rowid ).tryLock();
} else { } else {
saveGame( context, gamePtr, gi, lockDest, true ); saveGame( context, gamePtr, gi, lockDest, true );
} }
@ -158,16 +160,17 @@ public class GameUtils {
public static boolean resetGame( Context context, long rowidIn ) public static boolean resetGame( Context context, long rowidIn )
{ {
boolean success = false; boolean success = false;
GameLock lock = new GameLock( rowidIn, true ).lock( 500 ); try ( GameLock lock = GameLock.getFor( rowidIn ).lock( 500 ) ) {
if ( null != lock ) { if ( null != lock ) {
tellDied( context, lock, true ); tellDied( context, lock, true );
resetGame( context, lock, lock, false ); resetGame( context, lock, lock, false );
lock.unlock();
Utils.cancelNotification( context, (int)rowidIn ); Utils.cancelNotification( context, (int)rowidIn );
success = true; success = true;
} else { } else {
Log.w( TAG, "resetGame: unable to open rowid %d", rowidIn ); DbgUtils.toastNoLock( TAG, context, "resetGame(): rowid %d",
rowidIn );
}
} }
return success; return success;
} }
@ -216,13 +219,13 @@ public class GameUtils {
long maxMillis ) long maxMillis )
{ {
GameSummary result = null; GameSummary result = null;
JNIThread thread = JNIThread.getRetained( rowid ); JNIThread thread = JNIThread.getRetained( context, rowid );
GameLock lock = null; GameLock lock = null;
if ( null != thread ) { if ( null != thread ) {
lock = thread.getLock(); lock = thread.getLock();
} else { } else {
try { try {
lock = new GameLock( rowid, false ).lock( maxMillis ); lock = GameLock.getFor( rowid ).lockRO( maxMillis );
} catch ( GameLock.GameLockedException gle ) { } catch ( GameLock.GameLockedException gle ) {
Log.ex( TAG, gle ); Log.ex( TAG, gle );
} }
@ -249,11 +252,11 @@ public class GameUtils {
long rowid = DBUtils.ROWID_NOTFOUND; long rowid = DBUtils.ROWID_NOTFOUND;
GameLock lockSrc = null; GameLock lockSrc = null;
JNIThread thread = JNIThread.getRetained( rowidIn, false ); JNIThread thread = JNIThread.getRetained( context, rowidIn );
if ( null != thread ) { if ( null != thread ) {
lockSrc = thread.getLock(); lockSrc = thread.getLock();
} else { } else {
lockSrc = new GameLock( rowidIn, false ).lock( 300 ); lockSrc = GameLock.getFor( rowidIn ).lockRO( 300 );
} }
if ( null != lockSrc ) { if ( null != lockSrc ) {
@ -275,9 +278,13 @@ public class GameUtils {
public static void deleteGame( Context context, GameLock lock, boolean informNow ) public static void deleteGame( Context context, GameLock lock, boolean informNow )
{ {
tellDied( context, lock, informNow ); if ( null != lock ) {
Utils.cancelNotification( context, (int)lock.getRowid() ); tellDied( context, lock, informNow );
DBUtils.deleteGame( context, lock ); Utils.cancelNotification( context, (int)lock.getRowid() );
DBUtils.deleteGame( context, lock );
} else {
Log.e( TAG, "deleteGame(): null lock; doing nothing" );
}
} }
public static boolean deleteGame( Context context, long rowid, public static boolean deleteGame( Context context, long rowid,
@ -285,14 +292,15 @@ public class GameUtils {
{ {
boolean success; boolean success;
// does this need to be synchronized? // does this need to be synchronized?
GameLock lock = new GameLock( rowid, true ); try ( GameLock lock = GameLock.getFor( rowid ).tryLock() ) {
if ( lock.tryLock() ) { if ( null != lock ) {
deleteGame( context, lock, informNow ); deleteGame( context, lock, informNow );
lock.unlock(); success = true;
success = true; } else {
} else { DbgUtils.toastNoLock( TAG, context, "deleteGame(): rowid %d",
Log.w( TAG, "deleteGame: unable to delete rowid %d", rowid ); rowid );
success = false; success = false;
}
} }
return success; return success;
} }
@ -389,16 +397,16 @@ public class GameUtils {
public static Bitmap loadMakeBitmap( Context context, long rowid ) public static Bitmap loadMakeBitmap( Context context, long rowid )
{ {
Bitmap thumb = null; Bitmap thumb = null;
GameLock lock = new GameLock( rowid, false ); try ( GameLock lock = GameLock.getFor( rowid ).tryLockRO() ) {
if ( lock.tryLock() ) { if ( null != lock ) {
CurGameInfo gi = new CurGameInfo( context ); CurGameInfo gi = new CurGameInfo( context );
GamePtr gamePtr = loadMakeGame( context, gi, lock ); GamePtr gamePtr = loadMakeGame( context, gi, lock );
if ( null != gamePtr ) { if ( null != gamePtr ) {
thumb = takeSnapshot( context, gamePtr, gi ); thumb = takeSnapshot( context, gamePtr, gi );
gamePtr.release(); gamePtr.release();
DBUtils.saveThumbnail( context, lock, thumb ); DBUtils.saveThumbnail( context, lock, thumb );
}
} }
lock.unlock();
} }
return thumb; return thumb;
} }
@ -504,9 +512,10 @@ public class GameUtils {
CurGameInfo gi, long groupID ) CurGameInfo gi, long groupID )
{ {
byte[] stream = XwJNI.game_saveToStream( gamePtr, gi ); byte[] stream = XwJNI.game_saveToStream( gamePtr, gi );
GameLock lock = DBUtils.saveNewGame( context, stream, groupID, null ); long rowid;
long rowid = lock.getRowid(); try ( GameLock lock = DBUtils.saveNewGame( context, stream, groupID, null ) ) {
lock.unlock(); rowid = lock.getRowid();
}
return rowid; return rowid;
} }
@ -537,10 +546,10 @@ public class GameUtils {
long rowid = DBUtils.ROWID_NOTFOUND; long rowid = DBUtils.ROWID_NOTFOUND;
byte[] bytes = XwJNI.gi_to_stream( gi ); byte[] bytes = XwJNI.gi_to_stream( gi );
if ( null != bytes ) { if ( null != bytes ) {
GameLock lock = DBUtils.saveNewGame( context, bytes, groupID, try ( GameLock lock = DBUtils.saveNewGame( context, bytes, groupID,
gameName ); gameName ) ) {
rowid = lock.getRowid(); rowid = lock.getRowid();
lock.unlock(); }
} }
return rowid; return rowid;
} }
@ -638,65 +647,17 @@ public class GameUtils {
} }
if ( DBUtils.ROWID_NOTFOUND != rowid ) { if ( DBUtils.ROWID_NOTFOUND != rowid ) {
GameLock lock = new GameLock( rowid, true ).lock(); // Use tryLock in case we're on UI thread. It's guaranteed to
applyChanges( context, sink, gi, util, addr, null, lock, false ); // succeed because we just created the rowid.
lock.unlock(); try ( GameLock lock = GameLock.getFor( rowid ).tryLock() ) {
Assert.assertNotNull( lock );
applyChanges( context, sink, gi, util, addr, null, lock, false );
}
} }
return rowid; return rowid;
} }
public static long makeNewGame( Context context, MultiMsgSink sink,
int gameID, CommsAddrRec addr, int lang,
String dict, int nPlayersT,
int nPlayersH, int forceChannel,
String gameName )
{
return makeNewGame( context, sink, DBUtils.GROUPID_UNSPEC, gameID, addr,
lang, dict, nPlayersT, nPlayersH, forceChannel,
gameName );
}
public static long makeNewGame( Context context, int gameID,
CommsAddrRec addr, int lang,
String dict, int nPlayersT,
int nPlayersH, int forceChannel,
String gameName )
{
return makeNewGame( context, DBUtils.GROUPID_UNSPEC, gameID, addr,
lang, dict, nPlayersT, nPlayersH, forceChannel,
gameName );
}
public static long makeNewGame( Context context, long groupID, int gameID,
CommsAddrRec addr, int lang, String dict,
int nPlayersT, int nPlayersH,
int forceChannel, String gameName )
{
return makeNewGame( context, null, groupID, gameID, addr, lang, dict,
nPlayersT, nPlayersH, forceChannel, gameName );
}
public static long makeNewGame( Context context, MultiMsgSink sink,
long groupID, int gameID, CommsAddrRec addr,
int lang, String dict,
int nPlayersT, int nPlayersH,
int forceChannel, String gameName )
{
long rowid = DBUtils.ROWID_NOTFOUND;
int[] langa = { lang };
String[] dicta = { dict };
boolean isHost = null == addr;
if ( isHost ) {
addr = new CommsAddrRec( null, null );
}
String inviteID = GameUtils.formatGameID( gameID );
return makeNewMultiGame( context, sink, (UtilCtxt)null, groupID, addr,
langa, dicta, null, nPlayersT, nPlayersH,
forceChannel, inviteID, gameID, gameName,
isHost );
}
// @SuppressLint({ "NewApi", "NewApi", "NewApi", "NewApi" }) // @SuppressLint({ "NewApi", "NewApi", "NewApi", "NewApi" })
// @SuppressWarnings("deprecation") // @SuppressWarnings("deprecation")
// @TargetApi(11) // @TargetApi(11)
@ -865,7 +826,7 @@ public class GameUtils {
return file.endsWith( XWConstants.GAME_EXTN ); return file.endsWith( XWConstants.GAME_EXTN );
} }
public static Bundle makeLaunchExtras( long rowid, boolean invited ) private static Bundle makeLaunchExtras( long rowid, boolean invited )
{ {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putLong( INTENT_KEY_ROWID, rowid ); bundle.putLong( INTENT_KEY_ROWID, rowid );
@ -949,54 +910,57 @@ public class GameUtils {
// have the lock and we'll never get it. Better to drop // have the lock and we'll never get it. Better to drop
// the message than fire the hung-lock assert. Messages // the message than fire the hung-lock assert. Messages
// belong in local pre-delivery storage anyway. // belong in local pre-delivery storage anyway.
GameLock lock = new GameLock( rowid, true ).lock( 150 ); try ( GameLock lock = GameLock.getFor( rowid ).lock( 150 ) ) {
if ( null != lock ) { if ( null != lock ) {
CurGameInfo gi = new CurGameInfo( context ); CurGameInfo gi = new CurGameInfo( context );
FeedUtilsImpl feedImpl = new FeedUtilsImpl( context, rowid ); FeedUtilsImpl feedImpl = new FeedUtilsImpl( context, rowid );
GamePtr gamePtr = loadMakeGame( context, gi, feedImpl, sink, lock ); GamePtr gamePtr = loadMakeGame( context, gi, feedImpl, sink, lock );
if ( null != gamePtr ) { if ( null != gamePtr ) {
XwJNI.comms_resendAll( gamePtr, false, false ); XwJNI.comms_resendAll( gamePtr, false, false );
Assert.assertNotNull( ret ); Assert.assertNotNull( ret );
draw = XwJNI.game_receiveMessage( gamePtr, msg, ret ); draw = XwJNI.game_receiveMessage( gamePtr, msg, ret );
XwJNI.comms_ackAny( gamePtr ); XwJNI.comms_ackAny( gamePtr );
// update gi to reflect changes due to messages // update gi to reflect changes due to messages
XwJNI.game_getGi( gamePtr, gi ); XwJNI.game_getGi( gamePtr, gi );
if ( draw && XWPrefs.getThumbEnabled( context ) ) { if ( draw && XWPrefs.getThumbEnabled( context ) ) {
Bitmap bitmap = takeSnapshot( context, gamePtr, gi ); Bitmap bitmap = takeSnapshot( context, gamePtr, gi );
DBUtils.saveThumbnail( context, lock, bitmap ); DBUtils.saveThumbnail( context, lock, bitmap );
} }
if ( null != bmr ) { if ( null != bmr ) {
if ( null != feedImpl.m_chat ) { if ( null != feedImpl.m_chat ) {
bmr.m_chat = feedImpl.m_chat; bmr.m_chat = feedImpl.m_chat;
bmr.m_chatFrom = feedImpl.m_chatFrom; bmr.m_chatFrom = feedImpl.m_chatFrom;
bmr.m_chatTs = feedImpl.m_ts; bmr.m_chatTs = feedImpl.m_ts;
} else { } else {
LastMoveInfo lmi = new LastMoveInfo(); LastMoveInfo lmi = new LastMoveInfo();
XwJNI.model_getPlayersLastScore( gamePtr, -1, lmi ); XwJNI.model_getPlayersLastScore( gamePtr, -1, lmi );
bmr.m_lmi = lmi; bmr.m_lmi = lmi;
}
}
saveGame( context, gamePtr, gi, lock, false );
GameSummary summary = summarizeAndRelease( context, lock,
gamePtr, gi );
if ( null != isLocalOut ) {
isLocalOut[0] = 0 <= summary.turn
&& gi.players[summary.turn].isLocal;
}
int flags = setFromFeedImpl( feedImpl );
if ( GameSummary.MSG_FLAGS_NONE != flags ) {
draw = true;
int curFlags = DBUtils.getMsgFlags( context, rowid );
DBUtils.setMsgFlags( rowid, flags | curFlags );
} }
} }
saveGame( context, gamePtr, gi, lock, false );
GameSummary summary = summarizeAndRelease( context, lock,
gamePtr, gi );
if ( null != isLocalOut ) {
isLocalOut[0] = 0 <= summary.turn
&& gi.players[summary.turn].isLocal;
}
int flags = setFromFeedImpl( feedImpl );
if ( GameSummary.MSG_FLAGS_NONE != flags ) {
draw = true;
int curFlags = DBUtils.getMsgFlags( context, rowid );
DBUtils.setMsgFlags( rowid, flags | curFlags );
}
} }
lock.unlock(); } catch ( GameLock.GameLockedException gle ) {
DbgUtils.toastNoLock( TAG, context, "feedMessage(): dropping message "
+ " for %d", rowid );
} }
} }
return draw; return draw;
@ -1008,35 +972,37 @@ public class GameUtils {
public static boolean replaceDicts( Context context, long rowid, public static boolean replaceDicts( Context context, long rowid,
String oldDict, String newDict ) String oldDict, String newDict )
{ {
GameLock lock = new GameLock( rowid, true ).lock(300); boolean success;
boolean success = null != lock; try ( GameLock lock = GameLock.getFor( rowid ).lock(300) ) {
if ( success ) { success = null != lock;
byte[] stream = savedGame( context, lock ); if ( success ) {
CurGameInfo gi = new CurGameInfo( context ); byte[] stream = savedGame( context, lock );
XwJNI.gi_from_stream( gi, stream ); CurGameInfo gi = new CurGameInfo( context );
XwJNI.gi_from_stream( gi, stream );
// first time required so dictNames() will work // first time required so dictNames() will work
gi.replaceDicts( context, newDict ); gi.replaceDicts( context, newDict );
String[] dictNames = gi.dictNames(); String[] dictNames = gi.dictNames();
DictUtils.DictPairs pairs = DictUtils.openDicts( context, DictUtils.DictPairs pairs = DictUtils.openDicts( context,
dictNames ); dictNames );
GamePtr gamePtr = GamePtr gamePtr =
XwJNI.initFromStream( rowid, stream, gi, dictNames, XwJNI.initFromStream( rowid, stream, gi, dictNames,
pairs.m_bytes, pairs.m_paths, pairs.m_bytes, pairs.m_paths,
gi.langName( context ), null, gi.langName( context ), null,
null, CommonPrefs.get( context ), null ); null, CommonPrefs.get( context ), null );
// second time required as game_makeFromStream can overwrite // second time required as game_makeFromStream can overwrite
gi.replaceDicts( context, newDict ); gi.replaceDicts( context, newDict );
saveGame( context, gamePtr, gi, lock, false ); saveGame( context, gamePtr, gi, lock, false );
summarizeAndRelease( context, lock, gamePtr, gi ); summarizeAndRelease( context, lock, gamePtr, gi );
lock.unlock(); } else {
} else { DbgUtils.toastNoLock( TAG, context, "replaceDicts(): rowid %d",
Log.w( TAG, "replaceDicts: unable to open rowid %d", rowid ); rowid );
}
} }
return success; return success;
} // replaceDicts } // replaceDicts
@ -1292,32 +1258,32 @@ public class GameUtils {
} }
} }
GameLock lock = new GameLock( rowid, false ); try ( GameLock lock = GameLock.getFor( rowid ).tryLockRO() ) {
if ( lock.tryLock() ) { if ( null != lock ) {
CurGameInfo gi = new CurGameInfo( m_context ); CurGameInfo gi = new CurGameInfo( m_context );
MultiMsgSink sink = new MultiMsgSink( m_context, rowid ); MultiMsgSink sink = new MultiMsgSink( m_context, rowid );
GamePtr gamePtr = loadMakeGame( m_context, gi, sink, lock ); GamePtr gamePtr = loadMakeGame( m_context, gi, sink, lock );
if ( null != gamePtr ) { if ( null != gamePtr ) {
int nSent = XwJNI.comms_resendAll( gamePtr, true, int nSent = XwJNI.comms_resendAll( gamePtr, true,
m_filter, false ); m_filter, false );
gamePtr.release(); gamePtr.release();
Log.d( TAG, "Resender.doInBackground(): sent %d " Log.d( TAG, "Resender.doInBackground(): sent %d "
+ "messages for rowid %d", nSent, rowid ); + "messages for rowid %d", nSent, rowid );
nSentTotal += sink.numSent(); nSentTotal += sink.numSent();
} else {
Log.d( TAG, "Resender.doInBackground(): loadMakeGame()"
+ " failed for rowid %d", rowid );
}
} else { } else {
Log.d( TAG, "Resender.doInBackground(): loadMakeGame()" JNIThread jniThread = JNIThread.getRetained( m_context, rowid );
+ " failed for rowid %d", rowid ); if ( null != jniThread ) {
} jniThread.handle( JNIThread.JNICmd.CMD_RESEND, false,
lock.unlock(); false, false );
} else { jniThread.release();
JNIThread jniThread = JNIThread.getRetained( rowid, false ); } else {
if ( null != jniThread ) { Log.w( TAG, "Resender.doInBackground: unable to unlock %d",
jniThread.handle( JNIThread.JNICmd.CMD_RESEND, false, rowid );
false, false ); }
jniThread.release();
} else {
Log.w( TAG, "Resender.doInBackground: unable to unlock %d",
rowid );
} }
} }
} }

View file

@ -44,7 +44,6 @@ import android.widget.LinearLayout;
import android.widget.ListView; import android.widget.ListView;
import android.widget.TextView; import android.widget.TextView;
import junit.framework.Assert;
import org.eehouse.android.xw4.DBUtils.GameChangeType; import org.eehouse.android.xw4.DBUtils.GameChangeType;
import org.eehouse.android.xw4.DBUtils.GameGroupInfo; import org.eehouse.android.xw4.DBUtils.GameGroupInfo;
@ -86,6 +85,7 @@ public class GamesListDelegate extends ListDelegateBase
private static final String RELAYIDS_EXTRA = "relayids"; private static final String RELAYIDS_EXTRA = "relayids";
private static final String ROWID_EXTRA = "rowid"; private static final String ROWID_EXTRA = "rowid";
private static final String BACKGROUND_EXTRA = "bkgrnd";
private static final String GAMEID_EXTRA = "gameid"; private static final String GAMEID_EXTRA = "gameid";
private static final String REMATCH_ROWID_EXTRA = "rm_rowid"; private static final String REMATCH_ROWID_EXTRA = "rm_rowid";
private static final String REMATCH_DICT_EXTRA = "rm_dict"; private static final String REMATCH_DICT_EXTRA = "rm_dict";
@ -987,6 +987,16 @@ public class GamesListDelegate extends ListDelegateBase
} ); } );
updateField(); updateField();
// RECEIVE_SMS is required now (Oreo/SDK_26) but wasn't
// before. There's logic elsewhere to ask for it AND SEND_SMS, but if
// the user's already granted SEND_SMS we can get RECEIVE_SMS just by
// asking (OS will grant without user interaction) since they're in
// the same group. So just do it now. This code can be removed
// later...
if ( Perms23.havePermission( Perm.SEND_SMS ) ) {
Perms23.tryGetPerms( this, Perm.RECEIVE_SMS, 0, Action.SKIP_CALLBACK );
}
} // init } // init
@Override @Override
@ -1221,14 +1231,14 @@ public class GamesListDelegate extends ListDelegateBase
final Object ... args ) final Object ... args )
{ {
switch( event ) { switch( event ) {
case HOST_PONGED: // case HOST_PONGED:
post( new Runnable() { // post( new Runnable() {
public void run() { // public void run() {
DbgUtils.showf( m_activity, // DbgUtils.showf( m_activity,
"Pong from %s", args[0].toString() ); // "Pong from %s", args[0].toString() );
} // }
}); // });
break; // break;
case BT_GAME_CREATED: case BT_GAME_CREATED:
post( new Runnable() { post( new Runnable() {
public void run() { public void run() {
@ -1661,9 +1671,11 @@ public class GamesListDelegate extends ListDelegateBase
enable = BoardDelegate.rematchSupported( m_activity, rowID ); enable = BoardDelegate.rematchSupported( m_activity, rowID );
Utils.setItemVisible( menu, R.id.games_game_rematch, enable ); Utils.setItemVisible( menu, R.id.games_game_rematch, enable );
enable = item.getSummary().isMultiGame() boolean isMultiGame = item.getSummary().isMultiGame();
enable = isMultiGame
&& (BuildConfig.DEBUG || XWPrefs.getDebugEnabled( m_activity )); && (BuildConfig.DEBUG || XWPrefs.getDebugEnabled( m_activity ));
Utils.setItemVisible( menu, R.id.games_game_invites, enable ); Utils.setItemVisible( menu, R.id.games_game_invites, enable );
Utils.setItemVisible( menu, R.id.games_game_netstats, isMultiGame );
enable = !m_launchedGames.contains( rowID ); enable = !m_launchedGames.contains( rowID );
Utils.setItemVisible( menu, R.id.games_game_delete, enable ); Utils.setItemVisible( menu, R.id.games_game_delete, enable );
@ -1767,7 +1779,7 @@ public class GamesListDelegate extends ListDelegateBase
return handled; return handled;
} }
private boolean handleSelGamesItem( int itemID, final long[] selRowIDs ) private boolean handleSelGamesItem( int itemID, long[] selRowIDs )
{ {
boolean handled = true; boolean handled = true;
boolean dropSels = false; boolean dropSels = false;
@ -1806,8 +1818,9 @@ public class GamesListDelegate extends ListDelegateBase
.show(); .show();
break; break;
case R.id.games_game_copy: case R.id.games_game_copy:
final long selRowID = selRowIDs[0];
final GameSummary smry = GameUtils.getSummary( m_activity, final GameSummary smry = GameUtils.getSummary( m_activity,
selRowIDs[0] ); selRowID );
if ( smry.inRelayGame() ) { if ( smry.inRelayGame() ) {
makeOkOnlyBuilder( R.string.no_copy_network ).show(); makeOkOnlyBuilder( R.string.no_copy_network ).show();
} else { } else {
@ -1816,14 +1829,14 @@ public class GamesListDelegate extends ListDelegateBase
public void run() { public void run() {
Activity self = m_activity; Activity self = m_activity;
byte[] stream = byte[] stream =
GameUtils.savedGame( self, selRowIDs[0] ); GameUtils.savedGame( self, selRowID );
long groupID = XWPrefs long groupID = XWPrefs
.getDefaultNewGameGroup( self ); .getDefaultNewGameGroup( self );
GameLock lock = try ( GameLock lock =
GameUtils.saveNewGame( self, stream, groupID ); GameUtils.saveNewGame( self, stream, groupID ) ) {
DBUtils.saveSummary( self, lock, smry ); DBUtils.saveSummary( self, lock, smry );
m_mySIS.selGames.add( lock.getRowid() ); m_mySIS.selGames.add( lock.getRowid() );
lock.unlock(); }
mkListAdapter(); mkListAdapter();
} }
}); });
@ -1838,6 +1851,10 @@ public class GamesListDelegate extends ListDelegateBase
showDialogFragment( DlgID.RENAME_GAME, selRowIDs[0] ); showDialogFragment( DlgID.RENAME_GAME, selRowIDs[0] );
break; break;
case R.id.games_game_netstats:
onStatusClicked( selRowIDs[0] );
break;
// DEBUG only // DEBUG only
case R.id.games_game_invites: case R.id.games_game_invites:
msg = GameUtils.getSummary( m_activity, selRowIDs[0] ) msg = GameUtils.getSummary( m_activity, selRowIDs[0] )
@ -2023,9 +2040,7 @@ public class GamesListDelegate extends ListDelegateBase
try { try {
hasDicts = GameUtils.gameDictsHere( m_activity, rowid, missingNames, hasDicts = GameUtils.gameDictsHere( m_activity, rowid, missingNames,
missingLang ); missingLang );
} catch ( GameUtils.NoSuchGameException nsge ) { } catch ( GameLock.GameLockedException | GameUtils.NoSuchGameException ex ) {
hasDicts = true; // irrelevant question
} catch ( GameLock.GameLockedException gle ) {
hasDicts = true; // irrelevant question hasDicts = true; // irrelevant question
} }
@ -2096,7 +2111,8 @@ public class GamesListDelegate extends ListDelegateBase
boolean haveDict; boolean haveDict;
try { try {
haveDict = GameUtils.gameDictsHere( m_activity, rowid ); haveDict = GameUtils.gameDictsHere( m_activity, rowid );
} catch ( GameLock.GameLockedException gle ) { } catch ( GameLock.GameLockedException
| GameUtils.NoSuchGameException gle ) {
haveDict = true; haveDict = true;
} }
if ( haveDict ) { if ( haveDict ) {
@ -2237,7 +2253,12 @@ public class GamesListDelegate extends ListDelegateBase
int lang = extras.getInt( REMATCH_LANG_EXTRA, -1 ); int lang = extras.getInt( REMATCH_LANG_EXTRA, -1 );
String json = extras.getString( REMATCH_PREFS_EXTRA ); String json = extras.getString( REMATCH_PREFS_EXTRA );
// Don't save rematch in Archive group
long groupID = DBUtils.getGroupForGame( m_activity, srcRowID ); long groupID = DBUtils.getGroupForGame( m_activity, srcRowID );
if ( groupID == DBUtils.getArchiveGroup( m_activity ) ) {
groupID = XWPrefs.getDefaultNewGameGroup( m_activity );
}
newid = GameUtils.makeNewMultiGame( m_activity, groupID, dict, newid = GameUtils.makeNewMultiGame( m_activity, groupID, dict,
lang, json, addrs, gameName ); lang, json, addrs, gameName );
DBUtils.addRematchInfo( m_activity, newid, btAddr, phone, DBUtils.addRematchInfo( m_activity, newid, btAddr, phone,
@ -2267,6 +2288,14 @@ public class GamesListDelegate extends ListDelegateBase
} }
} }
private void tryBackgroundIntent( Intent intent )
{
if ( intent.getBooleanExtra( BACKGROUND_EXTRA, false ) ) {
makeOkOnlyBuilder( R.string.btservice_expl )
.show();
}
}
private void askDefaultName() private void askDefaultName()
{ {
String name = CommonPrefs.getDefaultPlayerName( m_activity, 0, true ); String name = CommonPrefs.getDefaultPlayerName( m_activity, 0, true );
@ -2481,6 +2510,7 @@ public class GamesListDelegate extends ListDelegateBase
startRematch( intent ); startRematch( intent );
tryAlert( intent ); tryAlert( intent );
tryNFCIntent( intent ); tryNFCIntent( intent );
tryBackgroundIntent( intent );
} }
private void doOpenGame( Object[] params ) private void doOpenGame( Object[] params )
@ -2665,6 +2695,13 @@ public class GamesListDelegate extends ListDelegateBase
return intent; return intent;
} }
public static Intent makeBackgroundIntent( Context context )
{
Intent intent = makeSelfIntent( context );
intent.putExtra( BACKGROUND_EXTRA, true );
return intent;
}
public static Intent makeRowidIntent( Context context, long rowid ) public static Intent makeRowidIntent( Context context, long rowid )
{ {
Intent intent = makeSelfIntent( context ); Intent intent = makeSelfIntent( context );

View file

@ -27,7 +27,6 @@ import android.widget.ImageButton;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import junit.framework.Assert;
import org.eehouse.android.xw4.loc.LocUtils; import org.eehouse.android.xw4.loc.LocUtils;

View file

@ -26,7 +26,6 @@ import android.content.Intent;
import org.eehouse.android.xw4.DlgDelegate.Action; import org.eehouse.android.xw4.DlgDelegate.Action;
import junit.framework.Assert;
class HostDelegate extends DelegateBase { class HostDelegate extends DelegateBase {
private static final String ACTION = "ACTION"; private static final String ACTION = "ACTION";

View file

@ -31,7 +31,6 @@ import android.widget.Button;
import java.util.ArrayList; import java.util.ArrayList;
import junit.framework.Assert;
import org.eehouse.android.xw4.DlgDelegate.Action; import org.eehouse.android.xw4.DlgDelegate.Action;
import org.eehouse.android.xw4.DlgDelegate.ActionPair; import org.eehouse.android.xw4.DlgDelegate.ActionPair;

View file

@ -36,14 +36,17 @@ import android.widget.FrameLayout;
import android.widget.ListView; import android.widget.ListView;
import android.widget.Spinner; import android.widget.Spinner;
import android.widget.TextView; import android.widget.TextView;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import junit.framework.Assert;
abstract class InviteDelegate extends ListDelegateBase abstract class InviteDelegate extends ListDelegateBase
implements View.OnClickListener, implements View.OnClickListener,
ViewGroup.OnHierarchyChangeListener { ViewGroup.OnHierarchyChangeListener {
@ -54,34 +57,25 @@ abstract class InviteDelegate extends ListDelegateBase
String getDev(); // the string that identifies this item in results String getDev(); // the string that identifies this item in results
} }
protected static class TwoStringPair implements InviterItem { protected static class TwoStringPair implements InviterItem, Serializable {
public String str1; private String mDev;
public String str2; public String str2;
public TwoStringPair( String str1, String str2 ) { public TwoStringPair( String dev, String str2 ) {
this.str1 = str1; this.str2 = str2; mDev = dev; this.str2 = str2;
} }
public static TwoStringPair[] make( String[] names, String[] addrs ) public String getDev() { return mDev; }
{
TwoStringPair[] pairs = new TwoStringPair[names.length];
for ( int ii = 0; ii < pairs.length; ++ii ) {
pairs[ii] = new TwoStringPair( names[ii], addrs[ii] );
}
return pairs;
}
public String getDev() { return str1; }
public boolean equals( InviterItem item ) public boolean equals( InviterItem item )
{ {
boolean result = false; boolean result = false;
if ( null != item ) { if ( null != item ) {
TwoStringPair pair = (TwoStringPair)item; TwoStringPair pair = (TwoStringPair)item;
result = str1.equals( pair.str1 ) result = mDev.equals( pair.mDev )
&& ((null == str2 && null == pair.str2) && ((null == str2 && null == pair.str2)
|| str2.equals( pair.str2 ) ); || str2.equals( pair.str2 ) );
Log.d( TAG, "%s.equals(%s) => %b", str1, pair.str1, result ); Log.d( TAG, "%s.equals(%s) => %b", mDev, pair.mDev, result );
} }
return result; return result;
} }
@ -153,14 +147,17 @@ abstract class InviteDelegate extends ListDelegateBase
for ( int id : buttonBarItemIds ) { for ( int id : buttonBarItemIds ) {
bar.findViewById( id ).setOnClickListener( listener ); bar.findViewById( id ).setOnClickListener( listener );
} }
tryEnable();
} }
protected void updateListAdapter( InviterItem[] items ) protected void updateListAdapter( List<? extends InviterItem> items )
{ {
updateListAdapter( R.layout.two_strs_item, items ); updateListAdapter( R.layout.two_strs_item, items );
} }
protected void updateListAdapter( int itemId, InviterItem[] items ) protected void updateListAdapter( int itemId,
List<? extends InviterItem> items )
{ {
updateChecked( items ); updateChecked( items );
m_adapter = new InviteItemsAdapter( itemId, items ); m_adapter = new InviteItemsAdapter( itemId, items );
@ -174,13 +171,7 @@ abstract class InviteDelegate extends ListDelegateBase
} }
} }
protected void onBarButtonClicked( int id ) abstract void onBarButtonClicked( int id );
{
Assert.fail(); // subclass must implement
}
// Subclasses can do something here
protected void addToButtonBar( FrameLayout container ) {}
//////////////////////////////////////// ////////////////////////////////////////
// View.OnClickListener // View.OnClickListener
@ -246,7 +237,7 @@ abstract class InviteDelegate extends ListDelegateBase
protected void clearChecked() { m_checked.clear(); } protected void clearChecked() { m_checked.clear(); }
// Figure which previously-checked items belong in the new set. // Figure which previously-checked items belong in the new set.
private void updateChecked( InviterItem[] newItems ) private void updateChecked( List<? extends InviterItem> newItems )
{ {
Set<InviterItem> old = new HashSet<InviterItem>(); Set<InviterItem> old = new HashSet<InviterItem>();
old.addAll( m_checked ); old.addAll( m_checked );
@ -282,11 +273,13 @@ abstract class InviteDelegate extends ListDelegateBase
private InviterItem[] m_items; private InviterItem[] m_items;
private int m_itemId; private int m_itemId;
public InviteItemsAdapter( int itemID, InviterItem[] items ) public InviteItemsAdapter( int itemID, List<? extends InviterItem> items )
{ {
super( null == items? 0 : items.length ); super( null == items? 0 : items.size() );
m_itemId = itemID; m_itemId = itemID;
m_items = items; if ( null != items ) {
m_items = items.toArray( new InviterItem[items.size()] );
}
// m_items = new LinearLayout[getCount()]; // m_items = new LinearLayout[getCount()];
} }

View file

@ -23,21 +23,23 @@ package org.eehouse.android.xw4;
import java.util.Formatter; import java.util.Formatter;
public class Log { public class Log {
private static final String PRE_TAG = BuildConfig.FLAVOR + "-";
public static void d( String tag, String fmt, Object... args ) { public static void d( String tag, String fmt, Object... args ) {
String str = new Formatter().format( fmt, args ).toString(); String str = new Formatter().format( fmt, args ).toString();
android.util.Log.d( tag, str ); android.util.Log.d( PRE_TAG + tag, str );
} }
public static void w( String tag, String fmt, Object... args ) { public static void w( String tag, String fmt, Object... args ) {
String str = new Formatter().format( fmt, args ).toString(); String str = new Formatter().format( fmt, args ).toString();
android.util.Log.w( tag, str ); android.util.Log.w( PRE_TAG + tag, str );
} }
public static void e( String tag, String fmt, Object... args ) { public static void e( String tag, String fmt, Object... args ) {
String str = new Formatter().format( fmt, args ).toString(); String str = new Formatter().format( fmt, args ).toString();
android.util.Log.e( tag, str ); android.util.Log.e( PRE_TAG + tag, str );
} }
public static void i( String tag, String fmt, Object... args ) { public static void i( String tag, String fmt, Object... args ) {
String str = new Formatter().format( fmt, args ).toString(); String str = new Formatter().format( fmt, args ).toString();
android.util.Log.i( tag, str ); android.util.Log.i( PRE_TAG + tag, str );
} }
public static void ex( String tag, Exception exception ) public static void ex( String tag, Exception exception )
@ -45,5 +47,4 @@ public class Log {
w( tag, "Exception: %s", exception.toString() ); w( tag, "Exception: %s", exception.toString() );
DbgUtils.printStack( tag, exception.getStackTrace() ); DbgUtils.printStack( tag, exception.getStackTrace() );
} }
} }

View file

@ -26,7 +26,6 @@ import android.os.Bundle;
import android.support.v4.app.DialogFragment; import android.support.v4.app.DialogFragment;
import java.io.Serializable; import java.io.Serializable;
import junit.framework.Assert;
import org.eehouse.android.xw4.loc.LocUtils; import org.eehouse.android.xw4.loc.LocUtils;

View file

@ -36,7 +36,6 @@ import android.widget.LinearLayout;
import android.widget.ListView; import android.widget.ListView;
import android.widget.TextView; import android.widget.TextView;
import junit.framework.Assert;
import org.eehouse.android.xw4.loc.LocUtils; import org.eehouse.android.xw4.loc.LocUtils;

View file

@ -35,7 +35,6 @@ import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import junit.framework.Assert;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.ArrayList; import java.util.ArrayList;
@ -138,8 +137,8 @@ public class MainActivity extends XWActivity
} }
} ); } );
if ( BuildConfig.DEBUG ) { if ( BuildConfig.DEBUG ) {
DbgUtils.showf( this, "Putting off handling intent; %d waiting", Log.d( TAG, "Putting off handling intent; %d waiting",
m_runWhenSafe.size() ); m_runWhenSafe.size() );
} }
handled = true; handled = true;
} }

View file

@ -22,7 +22,6 @@ package org.eehouse.android.xw4;
import android.content.Context; import android.content.Context;
import junit.framework.Assert;
import org.eehouse.android.xw4.jni.CommsAddrRec; import org.eehouse.android.xw4.jni.CommsAddrRec;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType; import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType;

View file

@ -26,7 +26,6 @@ import android.content.DialogInterface.OnClickListener;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import junit.framework.Assert;
import org.eehouse.android.xw4.loc.LocUtils; import org.eehouse.android.xw4.loc.LocUtils;
@ -88,7 +87,6 @@ public class MultiService {
SMS_SEND_FAILED_NOPERMISSION, SMS_SEND_FAILED_NOPERMISSION,
BT_GAME_CREATED, BT_GAME_CREATED,
BT_ERR_COUNT,
RELAY_ALERT, RELAY_ALERT,
}; };

View file

@ -27,7 +27,6 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.text.TextUtils; import android.text.TextUtils;
import junit.framework.Assert;
import org.eehouse.android.xw4.DBUtils.NeedsNagInfo; import org.eehouse.android.xw4.DBUtils.NeedsNagInfo;
import org.eehouse.android.xw4.jni.GameSummary; import org.eehouse.android.xw4.jni.GameSummary;

View file

@ -39,7 +39,6 @@ import java.util.Iterator;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import junit.framework.Assert;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType; import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnTypeSet; import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnTypeSet;

View file

@ -29,7 +29,6 @@ import android.net.NetworkInfo;
import android.os.Build; import android.os.Build;
import android.os.Handler; import android.os.Handler;
import junit.framework.Assert;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType; import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType;
@ -54,17 +53,21 @@ public class NetStateCache {
public static void register( Context context, StateChangedIf proc ) public static void register( Context context, StateChangedIf proc )
{ {
initIfNot( context ); if ( Utils.isOnUIThread() ) {
synchronized( s_ifs ) { initIfNot( context );
s_ifs.add( proc ); synchronized( s_ifs ) {
s_ifs.add( proc );
}
} }
} }
public static void unregister( Context context, StateChangedIf proc ) public static void unregister( Context context, StateChangedIf proc )
{ {
initIfNot( context ); if ( Utils.isOnUIThread() ) {
synchronized( s_ifs ) { initIfNot( context );
s_ifs.remove( proc ); synchronized( s_ifs ) {
s_ifs.remove( proc );
}
} }
} }
@ -178,8 +181,6 @@ public class NetStateCache {
public PvtBroadcastReceiver() public PvtBroadcastReceiver()
{ {
DbgUtils.assertOnUIThread();
mHandler = new Handler();
mLastStateSent = s_netAvail; mLastStateSent = s_netAvail;
} }
@ -188,6 +189,11 @@ public class NetStateCache {
{ {
DbgUtils.assertOnUIThread(); DbgUtils.assertOnUIThread();
if ( null == mHandler ) {
DbgUtils.assertOnUIThread();
mHandler = new Handler();
}
if ( intent.getAction(). if ( intent.getAction().
equals( ConnectivityManager.CONNECTIVITY_ACTION)) { equals( ConnectivityManager.CONNECTIVITY_ACTION)) {

View file

@ -23,7 +23,6 @@ package org.eehouse.android.xw4;
import android.content.Context; import android.content.Context;
import android.text.TextUtils; import android.text.TextUtils;
import junit.framework.Assert;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
@ -276,8 +275,10 @@ public class NetUtils {
} }
result = new String( bas.toByteArray() ); result = new String( bas.toByteArray() );
} else { } else {
Log.w( TAG, "runConn: responseCode: %d for url: %s", Log.w( TAG, "runConn: responseCode: %d/%s for url: %s",
responseCode, conn.getURL() ); responseCode, conn.getResponseMessage(),
conn.getURL() );
logErrorStream( conn.getErrorStream() );
} }
} catch ( java.net.ProtocolException pe ) { } catch ( java.net.ProtocolException pe ) {
Log.ex( TAG, pe ); Log.ex( TAG, pe );
@ -289,6 +290,24 @@ public class NetUtils {
return result; return result;
} }
private static void logErrorStream( InputStream is )
{
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
for ( ; ; ) {
int length = is.read( buffer );
if ( length == -1 ) {
break;
}
baos.write( buffer, 0, length );
}
Log.e( TAG, baos.toString() );
} catch (Exception ex) {
Log.e( TAG, ex.getMessage() );
}
}
// This handles multiple params but only every gets passed one! // This handles multiple params but only every gets passed one!
private static String getPostDataString( Map<String, String> params ) private static String getPostDataString( Map<String, String> params )
{ {

View file

@ -30,7 +30,6 @@ import android.support.v4.app.DialogFragment;
import org.eehouse.android.xw4.DlgDelegate.Action; import org.eehouse.android.xw4.DlgDelegate.Action;
import org.eehouse.android.xw4.DlgDelegate.ActionPair; import org.eehouse.android.xw4.DlgDelegate.ActionPair;
import junit.framework.Assert;
import org.eehouse.android.xw4.loc.LocUtils; import org.eehouse.android.xw4.loc.LocUtils;

View file

@ -30,7 +30,6 @@ import android.support.v4.app.DialogFragment;
import org.eehouse.android.xw4.DlgDelegate.Action; import org.eehouse.android.xw4.DlgDelegate.Action;
import org.eehouse.android.xw4.DlgDelegate.ActionPair; import org.eehouse.android.xw4.DlgDelegate.ActionPair;
import junit.framework.Assert;
import org.eehouse.android.xw4.loc.LocUtils; import org.eehouse.android.xw4.loc.LocUtils;

View file

@ -30,10 +30,18 @@ public class OnBootReceiver extends BroadcastReceiver {
@Override @Override
public void onReceive( Context context, Intent intent ) public void onReceive( Context context, Intent intent )
{ {
if ( null != intent && null != intent.getAction() if ( null != intent ) {
&& intent.getAction().equals( Intent.ACTION_BOOT_COMPLETED ) ) { String action = intent.getAction();
Log.d( TAG, "got ACTION_BOOT_COMPLETED" ); Log.d( TAG, "got %s", action );
startTimers( context ); switch( action ) {
case Intent.ACTION_BOOT_COMPLETED:
case Intent.ACTION_MY_PACKAGE_REPLACED:
startTimers( context );
// Let's not put up the foreground service notification on
// boot. Too likely to annoy.
// BTService.onAppToBackground( context );
break;
}
} }
} }

View file

@ -19,6 +19,7 @@
package org.eehouse.android.xw4; package org.eehouse.android.xw4;
import android.Manifest;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.Context; import android.content.Context;
@ -30,6 +31,7 @@ import android.support.v4.content.ContextCompat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@ -38,20 +40,20 @@ import org.eehouse.android.xw4.DlgDelegate.DlgClickNotify;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType; import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType;
import org.eehouse.android.xw4.loc.LocUtils; import org.eehouse.android.xw4.loc.LocUtils;
import junit.framework.Assert;
public class Perms23 { public class Perms23 {
private static final String TAG = Perms23.class.getSimpleName(); private static final String TAG = Perms23.class.getSimpleName();
public static enum Perm { public static enum Perm {
READ_PHONE_STATE("android.permission.READ_PHONE_STATE"), READ_PHONE_STATE(Manifest.permission.READ_PHONE_STATE),
STORAGE("android.permission.WRITE_EXTERNAL_STORAGE"), STORAGE(Manifest.permission.WRITE_EXTERNAL_STORAGE),
SEND_SMS("android.permission.SEND_SMS"), SEND_SMS(Manifest.permission.SEND_SMS),
READ_CONTACTS("android.permission.READ_CONTACTS") RECEIVE_SMS(Manifest.permission.RECEIVE_SMS),
; READ_CONTACTS(Manifest.permission.READ_CONTACTS);
private String m_str; private String m_str;
private Perm(String str) { m_str = str; } private Perm(String str) { m_str = str; }
public String getString() { return m_str; } public String getString() { return m_str; }
public static Perm getFor( String str ) { public static Perm getFor( String str ) {
Perm result = null; Perm result = null;
@ -80,6 +82,12 @@ public class Perms23 {
m_perms.addAll( perms ); m_perms.addAll( perms );
} }
public Builder( Perm[] perms ) {
for ( Perm perm : perms ) {
m_perms.add( perm );
}
}
public Builder( Perm perm ) { public Builder( Perm perm ) {
m_perms.add( perm ); m_perms.add( perm );
} }
@ -128,7 +136,7 @@ public class Perms23 {
if ( haveAll ) { if ( haveAll ) {
if ( null != cbck ) { if ( null != cbck ) {
Map<Perm, Boolean> map = new HashMap<Perm, Boolean>(); Map<Perm, Boolean> map = new HashMap<>();
for ( Perm perm : m_perms ) { for ( Perm perm : m_perms ) {
map.put( perm, true ); map.put( perm, true );
} }
@ -149,36 +157,37 @@ public class Perms23 {
private static class QueryInfo { private static class QueryInfo {
private Action m_action; private Action m_action;
private Perm m_perm; private Perm[] m_perms;
private DelegateBase m_delegate; private DelegateBase m_delegate;
private String m_rationaleMsg; private String m_rationaleMsg;
private Object[] m_params; private Object[] m_params;
private QueryInfo( DelegateBase delegate, Action action, private QueryInfo( DelegateBase delegate, Action action,
Perm perm, String msg, Object[] params ) { Perm[] perms, String msg, Object[] params ) {
m_delegate = delegate; m_delegate = delegate;
m_action = action; m_action = action;
m_perm = perm; m_perms = perms;
m_rationaleMsg = msg; m_rationaleMsg = msg;
m_params = params; m_params = params;
} }
private QueryInfo( DelegateBase delegate, Object[] params ) private QueryInfo( DelegateBase delegate, Object[] params )
{ {
this( delegate, (Action)params[0], (Perm)params[1], (String)params[2], this( delegate, (Action)params[0], (Perm[])params[1], (String)params[2],
(Object[])params[3] ); (Object[])params[3] );
} }
private Object[] getParams() private Object[] getParams()
{ {
return new Object[] { m_action, m_perm, m_rationaleMsg, m_params }; return new Object[] { m_action, m_perms, m_rationaleMsg, m_params };
} }
private void doIt( boolean showRationale ) private void doIt( boolean showRationale )
{ {
Builder builder = new Builder( m_perm ); Builder builder = new Builder( m_perms );
if ( showRationale && null != m_rationaleMsg ) { if ( showRationale && null != m_rationaleMsg ) {
builder.setOnShowRationale( new OnShowRationale() { builder.setOnShowRationale( new OnShowRationale() {
@Override
public void onShouldShowRationale( Set<Perm> perms ) { public void onShouldShowRationale( Set<Perm> perms ) {
m_delegate.makeConfirmThenBuilder( m_rationaleMsg, m_delegate.makeConfirmThenBuilder( m_rationaleMsg,
Action.PERMS_QUERY ) Action.PERMS_QUERY )
@ -191,9 +200,21 @@ public class Perms23 {
} ); } );
} }
builder.asyncQuery( m_delegate.getActivity(), new PermCbck() { builder.asyncQuery( m_delegate.getActivity(), new PermCbck() {
@Override
public void onPermissionResult( Map<Perm, Boolean> perms ) { public void onPermissionResult( Map<Perm, Boolean> perms ) {
if ( Action.SKIP_CALLBACK != m_action ) { if ( Action.SKIP_CALLBACK != m_action ) {
if ( perms.get( m_perm ) ) { Set<Perm> keys = perms.keySet();
// We need all the sought perms to have been granted
boolean allGood = keys.size() == m_params.length;
for ( Iterator<Perm> iter = keys.iterator();
allGood && iter.hasNext(); ) {
if ( !perms.get(iter.next()) ) {
allGood = false;
}
}
if ( allGood ) {
m_delegate.onPosButton( m_action, m_params ); m_delegate.onPosButton( m_action, m_params );
} else { } else {
m_delegate.onNegButton( m_action, m_params ); m_delegate.onNegButton( m_action, m_params );
@ -231,22 +252,36 @@ public class Perms23 {
* Request permissions, giving rationale once, then call with action and * Request permissions, giving rationale once, then call with action and
* either positive or negative, the former if permission granted. * either positive or negative, the former if permission granted.
*/ */
public static void tryGetPerms( DelegateBase delegate, Perm perm, int rationaleId, public static void tryGetPerms( DelegateBase delegate, Perm[] perms, int rationaleId,
final Action action, Object... params ) final Action action, Object... params )
{ {
// Log.d( TAG, "tryGetPerms(%s)", perm.toString() ); // Log.d( TAG, "tryGetPerms(%s)", perm.toString() );
Context context = XWApp.getContext(); Context context = XWApp.getContext();
String msg = LocUtils.getString( context, rationaleId ); String msg = rationaleId == 0
tryGetPerms( delegate, perm, msg, action, params ); ? null : LocUtils.getString( context, rationaleId );
tryGetPerms( delegate, perms, msg, action, params );
}
public static void tryGetPerms( DelegateBase delegate, Perm[] perms,
String rationaleMsg, final Action action,
Object... params )
{
// Log.d( TAG, "tryGetPerms(%s)", perm.toString() );
new QueryInfo( delegate, action, perms, rationaleMsg, params )
.doIt( true );
} }
public static void tryGetPerms( DelegateBase delegate, Perm perm, public static void tryGetPerms( DelegateBase delegate, Perm perm,
String rationaleMsg, final Action action, String rationaleMsg, final Action action,
Object... params ) Object... params )
{ {
// Log.d( TAG, "tryGetPerms(%s)", perm.toString() ); tryGetPerms( delegate, new Perm[]{ perm }, rationaleMsg, action, params );
new QueryInfo( delegate, action, perm, rationaleMsg, params ) }
.doIt( true );
public static void tryGetPerms( DelegateBase delegate, Perm perm, int rationaleId,
final Action action, Object... params )
{
tryGetPerms( delegate, new Perm[]{perm}, rationaleId, action, params );
} }
public static void onGotPermsAction( DelegateBase delegate, boolean positive, public static void onGotPermsAction( DelegateBase delegate, boolean positive,
@ -262,6 +297,7 @@ public class Perms23 {
{ {
// Log.d( TAG, "gotPermissionResult(%s)", perms.toString() ); // Log.d( TAG, "gotPermissionResult(%s)", perms.toString() );
Map<Perm, Boolean> result = new HashMap<Perm, Boolean>(); Map<Perm, Boolean> result = new HashMap<Perm, Boolean>();
boolean shouldResend = false;
for ( int ii = 0; ii < perms.length; ++ii ) { for ( int ii = 0; ii < perms.length; ++ii ) {
Perm perm = Perm.getFor( perms[ii] ); Perm perm = Perm.getFor( perms[ii] );
boolean granted = PackageManager.PERMISSION_GRANTED == granteds[ii]; boolean granted = PackageManager.PERMISSION_GRANTED == granteds[ii];
@ -270,9 +306,8 @@ public class Perms23 {
// Hack. If SMS has been granted, resend all moves. This should be // Hack. If SMS has been granted, resend all moves. This should be
// replaced with an api allowing listeners to register // replaced with an api allowing listeners to register
// Perm-by-Perm, but I'm in a hurry. // Perm-by-Perm, but I'm in a hurry.
if ( granted && perm == Perm.SEND_SMS ) { if ( granted && (perm == Perm.SEND_SMS || perm == Perm.RECEIVE_SMS) ) {
GameUtils.resendAllIf( context, CommsConnType.COMMS_CONN_SMS, shouldResend = true;
true, true );
} }
// Log.d( TAG, "calling %s.onPermissionResult(%s, %b)", // Log.d( TAG, "calling %s.onPermissionResult(%s, %b)",
@ -280,6 +315,11 @@ public class Perms23 {
// granted ); // granted );
} }
if ( shouldResend ) {
GameUtils.resendAllIf( context, CommsConnType.COMMS_CONN_SMS,
true, true );
}
PermCbck cbck = s_map.remove( code ); PermCbck cbck = s_map.remove( code );
if ( null != cbck ) { if ( null != cbck ) {
cbck.onPermissionResult( result ); cbck.onPermissionResult( result );
@ -291,15 +331,15 @@ public class Perms23 {
String permString = perm.getString(); String permString = perm.getString();
boolean result = PackageManager.PERMISSION_GRANTED boolean result = PackageManager.PERMISSION_GRANTED
== ContextCompat.checkSelfPermission( XWApp.getContext(), permString ); == ContextCompat.checkSelfPermission( XWApp.getContext(), permString );
// Log.d( TAG, "havePermission(%s) => %b", permString, result );
return result; return result;
} }
// This is probably overkill as the OS only allows one permission request // If two permission requests are made in a row the map may contain more
// at a time // than one entry.
private static int s_nextRecord = 0; private static int s_nextRecord = 0;
private static int register( PermCbck cbck ) private static int register( PermCbck cbck )
{ {
Assert.assertTrue( !BuildConfig.DEBUG || 0 == s_map.size() );
DbgUtils.assertOnUIThread(); DbgUtils.assertOnUIThread();
int code = ++s_nextRecord; int code = ++s_nextRecord;
s_map.put( code, cbck ); s_map.put( code, cbck );

View file

@ -27,7 +27,6 @@ import android.os.Bundle;
import android.preference.PreferenceActivity; import android.preference.PreferenceActivity;
import android.support.v4.app.DialogFragment; import android.support.v4.app.DialogFragment;
import junit.framework.Assert;
import org.eehouse.android.xw4.DlgDelegate.Action; import org.eehouse.android.xw4.DlgDelegate.Action;
import org.eehouse.android.xw4.DlgDelegate.ConfirmThenBuilder; import org.eehouse.android.xw4.DlgDelegate.ConfirmThenBuilder;

View file

@ -36,7 +36,6 @@ import android.preference.PreferenceScreen;
import android.view.View; import android.view.View;
import android.widget.Button; import android.widget.Button;
import junit.framework.Assert;
import org.eehouse.android.xw4.DlgDelegate.Action; import org.eehouse.android.xw4.DlgDelegate.Action;
import org.eehouse.android.xw4.loc.LocUtils; import org.eehouse.android.xw4.loc.LocUtils;

View file

@ -52,7 +52,6 @@ import java.util.Set;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
import junit.framework.Assert;
import org.eehouse.android.xw4.DlgDelegate.Action; import org.eehouse.android.xw4.DlgDelegate.Action;
@ -464,7 +463,7 @@ public class RelayInviteDelegate extends InviteDelegate {
}); });
addSelf(); addSelf();
updateListAdapter( m_devIDRecs.toArray( new DevIDRec[m_devIDRecs.size()] ) ); updateListAdapter( m_devIDRecs );
tryEnable(); tryEnable();
} }

View file

@ -25,10 +25,9 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Build; import android.os.Build;
import android.os.Handler; import android.os.Handler;
import android.support.v4.app.JobIntentService;
import android.text.TextUtils; import android.text.TextUtils;
import junit.framework.Assert;
import org.eehouse.android.xw4.GameUtils.BackMoveResult; import org.eehouse.android.xw4.GameUtils.BackMoveResult;
import org.eehouse.android.xw4.MultiService.DictFetchOwner; import org.eehouse.android.xw4.MultiService.DictFetchOwner;
import org.eehouse.android.xw4.MultiService.MultiEvent; import org.eehouse.android.xw4.MultiService.MultiEvent;
@ -63,7 +62,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
public class RelayService extends XWService public class RelayService extends JobIntentService
implements NetStateCache.StateChangedIf { implements NetStateCache.StateChangedIf {
private static final String TAG = RelayService.class.getSimpleName(); private static final String TAG = RelayService.class.getSimpleName();
private static final int MAX_SEND = 1024; private static final int MAX_SEND = 1024;
@ -103,6 +102,7 @@ public class RelayService extends XWService
private static List<PacketData> s_packetsSentUDP = new ArrayList<>(); private static List<PacketData> s_packetsSentUDP = new ArrayList<>();
private static List<PacketData> s_packetsSentWeb = new ArrayList<>(); private static List<PacketData> s_packetsSentWeb = new ArrayList<>();
private static final PacketData sEOQPacket = new PacketData();
private static AtomicInteger s_nextPacketID = new AtomicInteger(); private static AtomicInteger s_nextPacketID = new AtomicInteger();
private static boolean s_gcmWorking = false; private static boolean s_gcmWorking = false;
private static boolean s_registered = false; private static boolean s_registered = false;
@ -113,13 +113,15 @@ public class RelayService extends XWService
static { resetBackoffTimer(); } static { resetBackoffTimer(); }
private Thread m_fetchThread = null; // no longer used private Thread m_fetchThread = null; // no longer used
private AtomicReference<UDPThreads> m_UDPThreadsRef = new AtomicReference<>(); private static final AtomicReference<UDPThreads> sUDPThreadsRef = new AtomicReference<>();
private Handler m_handler; private Handler m_handler;
private UDPThreads mThreads;
private Runnable m_onInactivity; private Runnable m_onInactivity;
private int m_maxIntervalSeconds = 0; private int m_maxIntervalSeconds = 0;
private long m_lastGamePacketReceived; private long m_lastGamePacketReceived;
private int m_nativeFailScore; private static AtomicInteger sNativeFailScore = new AtomicInteger();;
private boolean m_skipUPDSet; private static boolean sSkipUPDSet;
private RelayServiceHelper mHelper;
private static DevIDType s_curType = DevIDType.ID_TYPE_NONE; private static DevIDType s_curType = DevIDType.ID_TYPE_NONE;
private static long s_regStartTime = 0; private static long s_regStartTime = 0;
@ -190,15 +192,34 @@ public class RelayService extends XWService
public static void startService( Context context ) public static void startService( Context context )
{ {
Log.i( TAG, "startService()" );
Intent intent = getIntentTo( context, MsgCmds.UDP_CHANGED ); Intent intent = getIntentTo( context, MsgCmds.UDP_CHANGED );
context.startService( intent ); enqueueWork( context, intent );
}
// Must use the same lobID for all work enqueued for the same class
private final static int sJobID = RelayService.class.hashCode();
private static void enqueueWork( Context context, Intent intent )
{
Log.d( TAG, "calling enqueueWork(cmd=%s)", cmdFrom( intent ) );
enqueueWork( context, RelayService.class, sJobID, intent );
}
private static MsgCmds cmdFrom( Intent intent )
{
MsgCmds cmd;
try {
cmd = MsgCmds.values()[intent.getIntExtra( CMD_STR, -1 )];
} catch (Exception ex) { // OOB most likely
cmd = null;
}
return cmd;
} }
private static void stopService( Context context ) private static void stopService( Context context )
{ {
Intent intent = getIntentTo( context, MsgCmds.STOP ); Intent intent = getIntentTo( context, MsgCmds.STOP );
context.startService( intent ); enqueueWork( context, intent );
} }
public static void inviteRemote( Context context, int destDevID, public static void inviteRemote( Context context, int destDevID,
@ -206,7 +227,7 @@ public class RelayService extends XWService
{ {
int myDevID = DevID.getRelayDevIDInt( context ); int myDevID = DevID.getRelayDevIDInt( context );
if ( 0 != myDevID ) { if ( 0 != myDevID ) {
context.startService( getIntentTo( context, MsgCmds.INVITE ) enqueueWork( context, getIntentTo( context, MsgCmds.INVITE )
.putExtra( DEV_ID_SRC, myDevID ) .putExtra( DEV_ID_SRC, myDevID )
.putExtra( DEV_ID_DEST, destDevID ) .putExtra( DEV_ID_DEST, destDevID )
.putExtra( RELAY_ID, relayID ) .putExtra( RELAY_ID, relayID )
@ -217,13 +238,13 @@ public class RelayService extends XWService
public static void reset( Context context ) public static void reset( Context context )
{ {
Intent intent = getIntentTo( context, MsgCmds.RESET ); Intent intent = getIntentTo( context, MsgCmds.RESET );
context.startService( intent ); enqueueWork( context, intent );
} }
public static void timerFired( Context context ) public static void timerFired( Context context )
{ {
Intent intent = getIntentTo( context, MsgCmds.TIMER_FIRED ); Intent intent = getIntentTo( context, MsgCmds.TIMER_FIRED );
context.startService( intent ); enqueueWork( context, intent );
} }
public static int sendPacket( Context context, long rowid, byte[] msg ) public static int sendPacket( Context context, long rowid, byte[] msg )
@ -234,7 +255,7 @@ public class RelayService extends XWService
Intent intent = getIntentTo( context, MsgCmds.SEND ) Intent intent = getIntentTo( context, MsgCmds.SEND )
.putExtra( ROWID, rowid ) .putExtra( ROWID, rowid )
.putExtra( BINBUFFER, msg ); .putExtra( BINBUFFER, msg );
context.startService( intent ); enqueueWork( context, intent );
result = msg.length; result = msg.length;
} else { } else {
Log.w( TAG, "sendPacket: network down" ); Log.w( TAG, "sendPacket: network down" );
@ -251,7 +272,7 @@ public class RelayService extends XWService
.putExtra( ROWID, rowid ) .putExtra( ROWID, rowid )
.putExtra( RELAY_ID, relayID ) .putExtra( RELAY_ID, relayID )
.putExtra( BINBUFFER, msg ); .putExtra( BINBUFFER, msg );
context.startService( intent ); enqueueWork( context, intent );
result = msg.length; result = msg.length;
} }
return result; return result;
@ -266,18 +287,11 @@ public class RelayService extends XWService
{ {
Log.d( TAG, "receiveInvitation: got nli from %d: %s", srcDevID, Log.d( TAG, "receiveInvitation: got nli from %d: %s", srcDevID,
nli.toString() ); nli.toString() );
if ( !handleInvitation( nli, null, DictFetchOwner.OWNER_RELAY ) ) { if ( !mHelper.handleInvitation( nli, null, DictFetchOwner.OWNER_RELAY ) ) {
Log.d( TAG, "handleInvitation() failed" ); Log.d( TAG, "handleInvitation() failed" );
} }
} }
@Override
void postNotification( String device, int gameID, long rowid )
{
String body = LocUtils.getString( this, R.string.new_relay_body );
GameUtils.postInvitedNotification( this, gameID, body, rowid );
}
// Exists to get incoming data onto the main thread // Exists to get incoming data onto the main thread
private static void postData( Context context, long rowid, byte[] msg ) private static void postData( Context context, long rowid, byte[] msg )
{ {
@ -287,7 +301,7 @@ public class RelayService extends XWService
Intent intent = getIntentTo( context, MsgCmds.RECEIVE ) Intent intent = getIntentTo( context, MsgCmds.RECEIVE )
.putExtra( ROWID, rowid ) .putExtra( ROWID, rowid )
.putExtra( BINBUFFER, msg ); .putExtra( BINBUFFER, msg );
context.startService( intent ); enqueueWork( context, intent );
} else { } else {
Log.w( TAG, "postData(): Dropping message for rowid %d:" Log.w( TAG, "postData(): Dropping message for rowid %d:"
+ " not on device", rowid ); + " not on device", rowid );
@ -305,33 +319,29 @@ public class RelayService extends XWService
Intent intent = getIntentTo( context, MsgCmds.PROCESS_GAME_MSGS ) Intent intent = getIntentTo( context, MsgCmds.PROCESS_GAME_MSGS )
.putExtra( MSGS_ARR, msgs64 ) .putExtra( MSGS_ARR, msgs64 )
.putExtra( RELAY_ID, relayId ); .putExtra( RELAY_ID, relayId );
context.startService( intent ); enqueueWork( context, intent );
} }
public static void processDevMsgs( Context context, String[] msgs64 ) public static void processDevMsgs( Context context, String[] msgs64 )
{ {
Intent intent = getIntentTo( context, MsgCmds.PROCESS_DEV_MSGS ) Intent intent = getIntentTo( context, MsgCmds.PROCESS_DEV_MSGS )
.putExtra( MSGS_ARR, msgs64 ); .putExtra( MSGS_ARR, msgs64 );
context.startService( intent ); enqueueWork( context, intent );
} }
private static Intent getIntentTo( Context context, MsgCmds cmd ) private static Intent getIntentTo( Context context, MsgCmds cmd )
{ {
Intent intent = new Intent( context, RelayService.class ); Intent intent = new Intent( context, RelayService.class )
intent.putExtra( CMD_STR, cmd.ordinal() ); .putExtra( CMD_STR, cmd.ordinal() );
return intent; return intent;
} }
@Override
protected MultiMsgSink getSink( long rowid )
{
return new RelayMsgSink().setRowID( rowid );
}
@Override @Override
public void onCreate() public void onCreate()
{ {
Log.d( TAG, "%s.onCreate()", this );
super.onCreate(); super.onCreate();
mHelper = new RelayServiceHelper( this );
m_lastGamePacketReceived = m_lastGamePacketReceived =
XWPrefs.getPrefsLong( this, R.string.key_last_packet, XWPrefs.getPrefsLong( this, R.string.key_last_packet,
Utils.getCurSeconds() ); Utils.getCurSeconds() );
@ -339,7 +349,7 @@ public class RelayService extends XWService
m_handler = new Handler(); m_handler = new Handler();
m_onInactivity = new Runnable() { m_onInactivity = new Runnable() {
public void run() { public void run() {
Log.d( TAG, "m_onInactivity fired" ); // Log.d( TAG, "m_onInactivity fired" );
if ( !shouldMaintainConnection() ) { if ( !shouldMaintainConnection() ) {
NetStateCache.unregister( RelayService.this, NetStateCache.unregister( RelayService.this,
RelayService.this ); RelayService.this );
@ -349,109 +359,20 @@ public class RelayService extends XWService
} }
} }
}; };
mThreads = startUDPThreadsOnce();
m_skipUPDSet = XWPrefs.getSkipToWebAPI( this ); if ( null == mThreads ) {
stopSelf();
}
sSkipUPDSet = XWPrefs.getSkipToWebAPI( this );
} }
@Override @Override
public int onStartCommand( Intent intent, int flags, int startId ) public void onHandleWork( Intent intent )
{ {
Integer result = null; DbgUtils.assertOnUIThread( false );
if ( null != intent ) { Log.d( TAG, "%s.onHandleWork(cmd=%s)", this, cmdFrom( intent ) );
MsgCmds cmd; handleCommand( intent );
try {
cmd = MsgCmds.values()[intent.getIntExtra( CMD_STR, -1 )];
} catch (Exception ex) { // OOB most likely
cmd = null;
}
if ( null != cmd ) {
// Log.d( TAG, "onStartCommand(): cmd=%s", cmd.toString() );
switch( cmd ) {
case PROCESS_GAME_MSGS:
String[] relayIDs = new String[1];
relayIDs[0] = intent.getStringExtra( RELAY_ID );
long[] rowIDs = DBUtils.getRowIDsFor( this, relayIDs[0] );
if ( 0 < rowIDs.length ) {
byte[][][] msgs = expandMsgsArray( intent );
process( msgs, rowIDs, relayIDs );
}
break;
case PROCESS_DEV_MSGS:
byte[][][] msgss = expandMsgsArray( intent );
for ( byte[][] msgs : msgss ) {
for ( byte[] msg : msgs ) {
gotPacket( msg, true, false );
}
}
break;
case UDP_CHANGED:
startThreads();
break;
case RESET:
stopThreads();
startThreads();
break;
case UPGRADE:
UpdateCheckReceiver.checkVersions( this, false );
break;
case GOT_INVITE:
int srcDevID = intent.getIntExtra( INVITE_FROM, 0 );
NetLaunchInfo nli
= NetLaunchInfo.makeFrom( this, intent.getStringExtra(NLI_DATA) );
receiveInvitation( srcDevID, nli );
break;
case SEND:
case RECEIVE:
case SENDNOCONN:
startUDPThreadsIfNot();
long rowid = intent.getLongExtra( ROWID, -1 );
byte[] msg = intent.getByteArrayExtra( BINBUFFER );
if ( MsgCmds.SEND == cmd ) {
sendMessage( rowid, msg );
} else if ( MsgCmds.SENDNOCONN == cmd ) {
String relayID = intent.getStringExtra( RELAY_ID );
sendNoConnMessage( rowid, relayID, msg );
} else {
receiveMessage( this, rowid, null, msg, s_addr );
}
break;
case INVITE:
startUDPThreadsIfNot();
srcDevID = intent.getIntExtra( DEV_ID_SRC, 0 );
int destDevID = intent.getIntExtra( DEV_ID_DEST, 0 );
String relayID = intent.getStringExtra( RELAY_ID );
String nliData = intent.getStringExtra( NLI_DATA );
sendInvitation( srcDevID, destDevID, relayID, nliData );
break;
case TIMER_FIRED:
if ( !NetStateCache.netAvail( this ) ) {
Log.w( TAG, "not connecting: no network" );
} else if ( startFetchThreadIfNotUDP() ) {
// do nothing
} else if ( registerWithRelayIfNot() ) {
requestMessages();
}
RelayReceiver.setTimer( this );
break;
case STOP:
stopThreads();
stopSelf();
break;
default:
Assert.fail();
}
result = Service.START_STICKY;
}
}
if ( null == result ) {
result = Service.START_STICKY_COMPATIBILITY;
}
NetStateCache.register( this, this );
resetExitTimer(); resetExitTimer();
return result;
} }
@Override @Override
@ -461,8 +382,11 @@ public class RelayService extends XWService
long interval_millis = getMaxIntervalSeconds() * 1000; long interval_millis = getMaxIntervalSeconds() * 1000;
RelayReceiver.setTimer( this, interval_millis ); RelayReceiver.setTimer( this, interval_millis );
} }
stopThreads(); if ( null != mThreads ) {
mThreads.unsetService();
}
super.onDestroy(); super.onDestroy();
Log.d( TAG, "%s.onDestroy() DONE", this );
} }
// NetStateCache.StateChangedIf interface // NetStateCache.StateChangedIf interface
@ -471,6 +395,88 @@ public class RelayService extends XWService
startService( this ); // bad name: will *stop* threads too startService( this ); // bad name: will *stop* threads too
} }
private void handleCommand( Intent intent )
{
MsgCmds cmd = cmdFrom( intent );
if ( null != cmd ) {
// Log.d( TAG, "handleCommand(): cmd=%s", cmd.toString() );
switch( cmd ) {
case PROCESS_GAME_MSGS:
String[] relayIDs = new String[1];
relayIDs[0] = intent.getStringExtra( RELAY_ID );
long[] rowIDs = DBUtils.getRowIDsFor( this, relayIDs[0] );
if ( 0 < rowIDs.length ) {
byte[][][] msgs = expandMsgsArray( intent );
process( msgs, rowIDs, relayIDs );
}
break;
case PROCESS_DEV_MSGS:
byte[][][] msgss = expandMsgsArray( intent );
for ( byte[][] msgs : msgss ) {
for ( byte[] msg : msgs ) {
gotPacket( msg, true, false );
}
}
break;
case UDP_CHANGED:
startThreads();
break;
case RESET:
stopThreads();
startThreads();
break;
case UPGRADE:
UpdateCheckReceiver.checkVersions( this, false );
break;
case GOT_INVITE:
int srcDevID = intent.getIntExtra( INVITE_FROM, 0 );
NetLaunchInfo nli
= NetLaunchInfo.makeFrom( this, intent.getStringExtra(NLI_DATA) );
receiveInvitation( srcDevID, nli );
break;
case SEND:
case RECEIVE:
case SENDNOCONN:
startUDPThreadsOnce();
long rowid = intent.getLongExtra( ROWID, -1 );
byte[] msg = intent.getByteArrayExtra( BINBUFFER );
if ( MsgCmds.SEND == cmd ) {
sendMessage( rowid, msg );
} else if ( MsgCmds.SENDNOCONN == cmd ) {
String relayID = intent.getStringExtra( RELAY_ID );
sendNoConnMessage( rowid, relayID, msg );
} else {
mHelper.receiveMessage( this, rowid, null, msg, s_addr );
}
break;
case INVITE:
startUDPThreadsOnce();
srcDevID = intent.getIntExtra( DEV_ID_SRC, 0 );
int destDevID = intent.getIntExtra( DEV_ID_DEST, 0 );
String relayID = intent.getStringExtra( RELAY_ID );
String nliData = intent.getStringExtra( NLI_DATA );
sendInvitation( srcDevID, destDevID, relayID, nliData );
break;
case TIMER_FIRED:
if ( !NetStateCache.netAvail( this ) ) {
Log.w( TAG, "not connecting: no network" );
} else if ( startFetchThreadIfNotUDP() ) {
// do nothing
} else if ( registerWithRelayIfNot() ) {
requestMessages();
}
RelayReceiver.setTimer( this );
break;
case STOP:
stopThreads();
stopSelf();
break;
default:
Assert.assertFalse( BuildConfig.DEBUG );
}
}
}
private void setupNotifications( String[] relayIDs, BackMoveResult[] bmrs, private void setupNotifications( String[] relayIDs, BackMoveResult[] bmrs,
ArrayList<Boolean> locals ) ArrayList<Boolean> locals )
{ {
@ -511,6 +517,7 @@ public class RelayService extends XWService
{ {
while ( null != m_fetchThread ) { while ( null != m_fetchThread ) {
Log.w( TAG, "2: m_fetchThread NOT NULL; WHAT TO DO???" ); Log.w( TAG, "2: m_fetchThread NOT NULL; WHAT TO DO???" );
Assert.assertFalse( BuildConfig.DEBUG );
try { try {
Thread.sleep( 20 ); Thread.sleep( 20 );
} catch( java.lang.InterruptedException ie ) { } catch( java.lang.InterruptedException ie ) {
@ -519,24 +526,38 @@ public class RelayService extends XWService
} }
} }
private void startUDPThreadsIfNot() private UDPThreads startUDPThreadsOnce()
{ {
UDPThreads threads = null;
if ( XWApp.UDP_ENABLED && relayEnabled( this ) ) { if ( XWApp.UDP_ENABLED && relayEnabled( this ) ) {
synchronized ( m_UDPThreadsRef ) { synchronized ( sUDPThreadsRef ) {
if ( null == m_UDPThreadsRef.get() ) { threads = sUDPThreadsRef.get();
UDPThreads threads = new UDPThreads(); if ( null == threads ) {
m_UDPThreadsRef.set( threads ); threads = new UDPThreads();
sUDPThreadsRef.set( threads );
threads.start(); threads.start();
} }
threads.setService( this );
} }
} else { } else {
Log.i( TAG, "startUDPThreadsIfNot(): UDP disabled" ); Log.i( TAG, "startUDPThreadsOnce(): UDP disabled" );
} }
} // startUDPThreadsIfNot return threads;
} // startUDPThreadsOnce
private boolean skipNativeSend() private void stopUDPThreads()
{ {
boolean skip = m_nativeFailScore > UDP_FAIL_LIMIT || m_skipUPDSet; synchronized ( sUDPThreadsRef ) {
UDPThreads threads = sUDPThreadsRef.getAndSet( null );
if ( null != threads ) {
threads.stop();
}
}
}
private static boolean skipNativeSend()
{
boolean skip = sNativeFailScore.get() > UDP_FAIL_LIMIT || sSkipUPDSet;
// Log.d( TAG, "skipNativeSend(score=%d)) => %b", m_nativeFailScore, skip ); // Log.d( TAG, "skipNativeSend(score=%d)) => %b", m_nativeFailScore, skip );
return skip; return skip;
} }
@ -548,8 +569,8 @@ public class RelayService extends XWService
private void noteSent( PacketData packet, boolean fromUDP ) private void noteSent( PacketData packet, boolean fromUDP )
{ {
Log.d( TAG, "Sent (fromUDP=%b) packet: cmd=%s, id=%d", // Log.d( TAG, "Sent (fromUDP=%b) packet: cmd=%s, id=%d",
fromUDP, packet.m_cmd.toString(), packet.m_packetID ); // fromUDP, packet.m_cmd.toString(), packet.m_packetID );
if ( fromUDP || packet.m_cmd != XWRelayReg.XWPDEV_ACK ) { if ( fromUDP || packet.m_cmd != XWRelayReg.XWPDEV_ACK ) {
List<PacketData> list = fromUDP ? s_packetsSentUDP : s_packetsSentWeb; List<PacketData> list = fromUDP ? s_packetsSentUDP : s_packetsSentWeb;
synchronized( list ) { synchronized( list ) {
@ -562,25 +583,15 @@ public class RelayService extends XWService
{ {
long nowMS = System.currentTimeMillis(); long nowMS = System.currentTimeMillis();
List<PacketData> map = fromUDP ? s_packetsSentUDP : s_packetsSentWeb; List<PacketData> map = fromUDP ? s_packetsSentUDP : s_packetsSentWeb;
Log.d( TAG, "noteSent(fromUDP=%b): adding %d; size before: %d", // Log.d( TAG, "noteSent(fromUDP=%b): adding %d; size before: %d",
fromUDP, packets.size(), map.size() ); // fromUDP, packets.size(), map.size() );
for ( PacketData packet : packets ) { for ( PacketData packet : packets ) {
if ( fromUDP ) { if ( fromUDP ) {
packet.setSentMS( nowMS ); packet.setSentMS( nowMS );
} }
noteSent( packet, fromUDP ); noteSent( packet, fromUDP );
} }
Log.d( TAG, "noteSent(fromUDP=%b): size after: %d", fromUDP, map.size() ); // Log.d( TAG, "noteSent(fromUDP=%b): size after: %d", fromUDP, map.size() );
}
private void stopUDPThreadsIf()
{
DbgUtils.assertOnUIThread();
UDPThreads threads = m_UDPThreadsRef.getAndSet( null );
if ( null != threads ) {
threads.stop();
}
} }
// MIGHT BE Running on reader thread // MIGHT BE Running on reader thread
@ -595,14 +606,14 @@ public class RelayService extends XWService
if ( !skipAck ) { if ( !skipAck ) {
sendAckIf( header ); sendAckIf( header );
} }
Log.d( TAG, "gotPacket(): cmd=%s", header.m_cmd.toString() ); Log.d( TAG, "%s.gotPacket(): cmd=%s", this, header.m_cmd.toString() );
switch ( header.m_cmd ) { switch ( header.m_cmd ) {
case XWPDEV_UNAVAIL: case XWPDEV_UNAVAIL:
int unavail = dis.readInt(); int unavail = dis.readInt();
Log.i( TAG, "relay unvailable for another %d seconds", Log.i( TAG, "relay unvailable for another %d seconds",
unavail ); unavail );
String str = getVLIString( dis ); String str = getVLIString( dis );
postEvent( MultiEvent.RELAY_ALERT, str ); mHelper.postEvent( MultiEvent.RELAY_ALERT, str );
break; break;
case XWPDEV_ALERT: case XWPDEV_ALERT:
str = getVLIString( dis ); str = getVLIString( dis );
@ -647,20 +658,20 @@ public class RelayService extends XWService
break; break;
case XWPDEV_UPGRADE: case XWPDEV_UPGRADE:
intent = getIntentTo( this, MsgCmds.UPGRADE ); intent = getIntentTo( this, MsgCmds.UPGRADE );
startService( intent ); enqueueWork( this, intent );
break; break;
case XWPDEV_GOTINVITE: case XWPDEV_GOTINVITE:
resetBackoff = true; resetBackoff = true;
intent = getIntentTo( this, MsgCmds.GOT_INVITE );
int srcDevID = dis.readInt(); int srcDevID = dis.readInt();
byte[] nliData = new byte[dis.readShort()]; byte[] nliData = new byte[dis.readShort()];
dis.readFully( nliData ); dis.readFully( nliData );
NetLaunchInfo nli = XwJNI.nliFromStream( nliData ); NetLaunchInfo nli = XwJNI.nliFromStream( nliData );
intent.putExtra( INVITE_FROM, srcDevID );
String asStr = nli.toString(); String asStr = nli.toString();
Log.d( TAG, "got invitation: %s", asStr ); Log.d( TAG, "got invitation: %s", asStr );
intent.putExtra( NLI_DATA, asStr ); intent = getIntentTo( this, MsgCmds.GOT_INVITE )
startService( intent ); .putExtra( INVITE_FROM, srcDevID )
.putExtra( NLI_DATA, asStr );
enqueueWork( this, intent );
break; break;
case XWPDEV_ACK: case XWPDEV_ACK:
noteAck( vli2un( dis ), fromUDP ); noteAck( vli2un( dis ), fromUDP );
@ -892,8 +903,7 @@ public class RelayService extends XWService
private void postPacket( ByteArrayOutputStream bas, XWRelayReg cmd ) private void postPacket( ByteArrayOutputStream bas, XWRelayReg cmd )
{ {
startUDPThreadsIfNot(); UDPThreads threads = startUDPThreadsOnce();
UDPThreads threads = m_UDPThreadsRef.get();
if ( threads != null ) { if ( threads != null ) {
threads.add( new PacketData( bas, cmd ) ); threads.add( new PacketData( bas, cmd ) );
} }
@ -953,7 +963,7 @@ public class RelayService extends XWService
long rowid = rowIDs[ii]; long rowid = rowIDs[ii];
sink.setRowID( rowid ); sink.setRowID( rowid );
for ( byte[] msg : forOne ) { for ( byte[] msg : forOne ) {
receiveMessage( this, rowid, sink, msg, s_addr ); mHelper.receiveMessage( this, rowid, sink, msg, s_addr );
} }
} }
} }
@ -961,37 +971,72 @@ public class RelayService extends XWService
} }
} }
private class UDPThreads { private static class UDPThreads {
private DatagramSocket m_UDPSocket; private DatagramSocket m_UDPSocket;
private LinkedBlockingQueue<PacketData> m_queue = private LinkedBlockingQueue<PacketData> m_queue =
new LinkedBlockingQueue<PacketData>(); new LinkedBlockingQueue<PacketData>();
private Thread m_UDPReadThread; private Thread m_UDPReadThread;
private Thread m_UDPWriteThread; private Thread m_UDPWriteThread;
private RelayService[] mServiceHolder = {null};
UDPThreads() {} void setService( RelayService service )
{
synchronized ( mServiceHolder ) {
mServiceHolder[0] = service;
// unblock waiters for non-null Service
mServiceHolder.notifyAll();
}
}
// It will be a few milliseconds before all threads using the current
// Service instance are done. Blocking the UI thread here until that
// happened make for a laggy UI, so I'm going to see if we can
// continue to use the instance for a short while after returning.
void unsetService()
{
synchronized ( mServiceHolder ) {
mServiceHolder[0] = null;
}
}
private RelayService getService() throws InterruptedException
{
synchronized ( mServiceHolder ) {
while ( null == mServiceHolder[0] ) {
mServiceHolder.wait();
}
return mServiceHolder[0];
}
}
void start() void start()
{ {
m_UDPReadThread = new Thread( null, new Runnable() { m_UDPReadThread = new Thread( null, new Runnable() {
public void run() { public void run() {
try {
connectSocket(); // block until this is done
startWriteThread();
connectSocket(); // block until this is done Log.i( TAG, "read thread running" );
startWriteThread(); byte[] buf = new byte[1024];
for ( ; ; ) {
Log.i( TAG, "read thread running" ); DatagramPacket packet =
byte[] buf = new byte[1024]; new DatagramPacket( buf, buf.length );
for ( ; ; ) { try {
DatagramPacket packet = m_UDPSocket.receive( packet );
new DatagramPacket( buf, buf.length ); final RelayService service = getService();
try { service.resetExitTimer();
m_UDPSocket.receive( packet ); service.gotPacket( packet );
resetExitTimer(); } catch ( java.io.InterruptedIOException iioe ) {
gotPacket( packet ); // DbgUtils.logf( "FYI: udp receive timeout" );
} catch ( java.io.InterruptedIOException iioe ) { } catch( java.io.IOException ioe ) {
// DbgUtils.logf( "FYI: udp receive timeout" ); break;
} catch( java.io.IOException ioe ) { }
break;
} }
} catch ( InterruptedException ie ) {
Log.d( TAG, "exiting on interrupt: %s",
ie.getMessage() );
} }
Log.i( TAG, "read thread exiting" ); Log.i( TAG, "read thread exiting" );
} }
@ -1001,7 +1046,7 @@ public class RelayService extends XWService
void stop() void stop()
{ {
m_queue.add( new EOQPacketData() ); // will kill the writer thread m_queue.add( sEOQPacket ); // will kill the writer thread
} }
void add( PacketData packet ) void add( PacketData packet )
@ -1009,11 +1054,15 @@ public class RelayService extends XWService
m_queue.add( packet ); m_queue.add( packet );
} }
private void connectSocket() private void connectSocket() throws InterruptedException
{ {
if ( null == m_UDPSocket ) { if ( null == m_UDPSocket ) {
int port = XWPrefs.getDefaultRelayPort( RelayService.this ); int port;
String host = XWPrefs.getDefaultRelayHost( RelayService.this ); String host;
final RelayService service = getService();
port = XWPrefs.getDefaultRelayPort( service );
host = XWPrefs.getDefaultRelayHost( service );
try { try {
m_UDPSocket = new DatagramSocket(); m_UDPSocket = new DatagramSocket();
m_UDPSocket.setSoTimeout(30 * 1000); // timeout so we can log m_UDPSocket.setSoTimeout(30 * 1000); // timeout so we can log
@ -1051,7 +1100,7 @@ public class RelayService extends XWService
for ( outData = m_queue.poll(ts, TimeUnit.SECONDS); for ( outData = m_queue.poll(ts, TimeUnit.SECONDS);
null != outData; null != outData;
outData = m_queue.poll() ) { // doesn't block outData = m_queue.poll() ) { // doesn't block
if ( outData instanceof EOQPacketData ) { if ( outData == sEOQPacket ) {
gotEOQ = true; gotEOQ = true;
break; break;
} else if ( skipNativeSend() || outData.getForWeb() ) { } else if ( skipNativeSend() || outData.getForWeb() ) {
@ -1060,18 +1109,19 @@ public class RelayService extends XWService
dataListUDP.add( outData ); dataListUDP.add( outData );
} }
} }
sendViaWeb( dataListWeb );
sendViaUDP( dataListUDP );
getService().resetExitTimer();
runUDPAckTimer();
ConnStatusHandler.showSuccessOut();
} catch ( InterruptedException ie ) { } catch ( InterruptedException ie ) {
Log.w( TAG, "write thread killed" ); Log.w( TAG, "write thread killed" );
break; break;
} }
sendViaWeb( dataListWeb );
sendViaUDP( dataListUDP );
resetExitTimer();
runUDPAckTimer();
ConnStatusHandler.showSuccessOut();
} }
Log.i( TAG, "write thread killing read thread" ); Log.i( TAG, "write thread killing read thread" );
@ -1090,19 +1140,22 @@ public class RelayService extends XWService
m_UDPWriteThread.start(); m_UDPWriteThread.start();
} }
private int sendViaWeb( List<PacketData> packets ) private int sendViaWeb( List<PacketData> packets ) throws InterruptedException
{ {
Log.d( TAG, "sendViaWeb(): sending %d at once", packets.size() );
int sentLen = 0; int sentLen = 0;
if ( packets.size() > 0 ) { if ( packets.size() > 0 ) {
HttpURLConnection conn = NetUtils.makeHttpRelayConn( RelayService.this, "post" ); Log.d( TAG, "sendViaWeb(): sending %d at once", packets.size() );
final RelayService service = getService();
HttpURLConnection conn = NetUtils
.makeHttpRelayConn( service, "post" );
if ( null == conn ) { if ( null == conn ) {
Log.e( TAG, "sendViaWeb(): null conn for POST" ); Log.e( TAG, "sendViaWeb(): null conn for POST" );
} else { } else {
try { try {
JSONArray dataArray = new JSONArray(); JSONArray dataArray = new JSONArray();
for ( PacketData packet : packets ) { for ( PacketData packet : packets ) {
Assert.assertFalse( packet instanceof EOQPacketData ); Assert.assertFalse( packet == sEOQPacket );
byte[] datum = packet.assemble(); byte[] datum = packet.assemble();
dataArray.put( Utils.base64Encode(datum) ); dataArray.put( Utils.base64Encode(datum) );
sentLen += datum.length; sentLen += datum.length;
@ -1119,19 +1172,20 @@ public class RelayService extends XWService
int nReplies = resData.length(); int nReplies = resData.length();
// Log.d( TAG, "sendViaWeb(): got %d replies", nReplies ); // Log.d( TAG, "sendViaWeb(): got %d replies", nReplies );
noteSent( packets, false ); // before we process the acks below :-) service
.noteSent( packets, false ); // before we process the acks below :-)
for ( int ii = 0; ii < nReplies; ++ii ) { for ( int ii = 0; ii < nReplies; ++ii ) {
byte[] datum = Utils.base64Decode( resData.getString( ii ) ); byte[] datum = Utils.base64Decode( resData.getString( ii ) );
// PENDING: skip ack or not // PENDING: skip ack or not
gotPacket( datum, false, false ); service.gotPacket( datum, false, false );
} }
} else { } else {
Log.e( TAG, "sendViaWeb(): failed result for POST" ); Log.e( TAG, "sendViaWeb(): failed result for POST" );
} }
ConnStatusHandler.updateStatus( RelayService.this, null, ConnStatusHandler.updateStatus( service, null,
CommsConnType.COMMS_CONN_RELAY, CommsConnType.COMMS_CONN_RELAY,
succeeded ); succeeded );
} catch ( JSONException ex ) { } catch ( JSONException ex ) {
@ -1142,12 +1196,13 @@ public class RelayService extends XWService
return sentLen; return sentLen;
} }
private int sendViaUDP( List<PacketData> packets ) private int sendViaUDP( List<PacketData> packets ) throws InterruptedException
{ {
int sentLen = 0; int sentLen = 0;
if ( packets.size() > 0 ) { if ( packets.size() > 0 ) {
noteSent( packets, true ); final RelayService service = getService();
service.noteSent( packets, true );
for ( PacketData packet : packets ) { for ( PacketData packet : packets ) {
boolean getOut = true; boolean getOut = true;
byte[] data = packet.assemble(); byte[] data = packet.assemble();
@ -1161,13 +1216,13 @@ public class RelayService extends XWService
} catch ( java.net.SocketException se ) { } catch ( java.net.SocketException se ) {
Log.ex( TAG, se ); Log.ex( TAG, se );
Log.i( TAG, "Restarting threads to force new socket" ); Log.i( TAG, "Restarting threads to force new socket" );
ConnStatusHandler.updateStatusOut( RelayService.this, null, ConnStatusHandler.updateStatusOut( service, null,
CommsConnType.COMMS_CONN_RELAY, CommsConnType.COMMS_CONN_RELAY,
true ); true );
m_handler.post( new Runnable() { service.m_handler.post( new Runnable() {
public void run() { public void run() {
stopUDPThreadsIf(); service.stopUDPThreads();
} }
} ); } );
break; break;
@ -1181,7 +1236,7 @@ public class RelayService extends XWService
} }
} }
ConnStatusHandler.updateStatus( RelayService.this, null, ConnStatusHandler.updateStatus( service, null,
CommsConnType.COMMS_CONN_RELAY, CommsConnType.COMMS_CONN_RELAY,
sentLen > 0 ); sentLen > 0 );
} }
@ -1216,7 +1271,7 @@ public class RelayService extends XWService
forResend.add( packet ); forResend.add( packet );
if ( packet.m_cmd != XWRelayReg.XWPDEV_ACK ) { if ( packet.m_cmd != XWRelayReg.XWPDEV_ACK ) {
foundNonAck = true; foundNonAck = true;
++m_nativeFailScore; sNativeFailScore.incrementAndGet();
} }
iter.remove(); iter.remove();
} }
@ -1393,13 +1448,13 @@ public class RelayService extends XWService
// Log.d( TAG, "noteAck(fromUDP=%b): removed for id %d: %s", // Log.d( TAG, "noteAck(fromUDP=%b): removed for id %d: %s",
// fromUDP, packetID, packet ); // fromUDP, packetID, packet );
if ( fromUDP ) { if ( fromUDP ) {
--m_nativeFailScore; sNativeFailScore.decrementAndGet();
} }
} else { } else {
Log.w( TAG, "Weird: got ack %d but never sent", packetID ); Log.w( TAG, "Weird: got ack %d but never sent", packetID );
} }
if ( BuildConfig.DEBUG ) { if ( false && BuildConfig.DEBUG ) {
ArrayList<String> pstrs = new ArrayList<>(); ArrayList<String> pstrs = new ArrayList<>();
for ( PacketData datum : map ) { for ( PacketData datum : map ) {
if ( 0 != datum.m_packetID ) { if ( 0 != datum.m_packetID ) {
@ -1422,6 +1477,7 @@ public class RelayService extends XWService
// Called from any thread // Called from any thread
private void resetExitTimer() private void resetExitTimer()
{ {
// Log.d( TAG, "resetExitTimer()" );
m_handler.removeCallbacks( m_onInactivity ); m_handler.removeCallbacks( m_onInactivity );
// UDP socket's no good as a return address after several // UDP socket's no good as a return address after several
@ -1437,10 +1493,11 @@ public class RelayService extends XWService
stopThreads(); stopThreads();
} else if ( XWApp.UDP_ENABLED ) { } else if ( XWApp.UDP_ENABLED ) {
stopFetchThreadIf(); stopFetchThreadIf();
startUDPThreadsIfNot(); startUDPThreadsOnce();
registerWithRelay(); registerWithRelay();
} else { } else {
stopUDPThreadsIf(); Assert.assertFalse( BuildConfig.DEBUG );
stopUDPThreads();
startFetchThreadIfNotUDP(); startFetchThreadIfNotUDP();
} }
} }
@ -1449,7 +1506,7 @@ public class RelayService extends XWService
{ {
Log.d( TAG, "stopThreads()" ); Log.d( TAG, "stopThreads()" );
stopFetchThreadIf(); stopFetchThreadIf();
stopUDPThreadsIf(); stopUDPThreads();
} }
private static void un2vli( int nn, OutputStream os ) private static void un2vli( int nn, OutputStream os )
@ -1528,7 +1585,8 @@ public class RelayService extends XWService
result = figureBackoffSeconds(); result = figureBackoffSeconds();
} }
Log.d( TAG, "getMaxIntervalSeconds() => %d", result ); // WFT? went from 40 to 1000 // WFT? went from 40 to 1000
// Log.d( TAG, "getMaxIntervalSeconds() => %d", result );
return result; return result;
} }
@ -1616,7 +1674,7 @@ public class RelayService extends XWService
} }
} }
private class PacketData { private static class PacketData {
public ByteArrayOutputStream m_bas; public ByteArrayOutputStream m_bas;
public XWRelayReg m_cmd; public XWRelayReg m_cmd;
public byte[] m_header; public byte[] m_header;
@ -1670,8 +1728,8 @@ public class RelayService extends XWService
try { try {
m_packetID = nextPacketID( m_cmd ); m_packetID = nextPacketID( m_cmd );
DataOutputStream out = new DataOutputStream( bas ); DataOutputStream out = new DataOutputStream( bas );
Log.d( TAG, "makeHeader(): building packet with cmd %s", // Log.d( TAG, "makeHeader(): building packet with cmd %s",
m_cmd.toString() ); // m_cmd.toString() );
out.writeByte( XWPDevProto.XWPDEV_PROTO_VERSION_1.ordinal() ); out.writeByte( XWPDevProto.XWPDEV_PROTO_VERSION_1.ordinal() );
un2vli( m_packetID, out ); un2vli( m_packetID, out );
out.writeByte( m_cmd.ordinal() ); out.writeByte( m_cmd.ordinal() );
@ -1682,6 +1740,25 @@ public class RelayService extends XWService
} }
} }
// Exits only to exist, so instanceof can distinguish private class RelayServiceHelper extends XWServiceHelper {
private class EOQPacketData extends PacketData {}
private Service mService;
RelayServiceHelper( Service service ) {
super( service );
mService = service;
}
@Override
protected MultiMsgSink getSink( long rowid )
{
return new RelayMsgSink().setRowID( rowid );
}
@Override
void postNotification( String device, int gameID, long rowid )
{
String body = LocUtils.getString( mService, R.string.new_relay_body );
GameUtils.postInvitedNotification( mService, gameID, body, rowid );
}
}
} }

View file

@ -36,7 +36,6 @@ import android.view.View;
import android.widget.Button; import android.widget.Button;
import android.widget.EditText; import android.widget.EditText;
import junit.framework.Assert;
import org.eehouse.android.xw4.DBUtils.SentInvitesInfo; import org.eehouse.android.xw4.DBUtils.SentInvitesInfo;
import org.eehouse.android.xw4.DlgDelegate.Action; import org.eehouse.android.xw4.DlgDelegate.Action;
@ -299,7 +298,7 @@ public class SMSInviteDelegate extends InviteDelegate {
} }
}); });
updateListAdapter( m_phoneRecs.toArray( new PhoneRec[m_phoneRecs.size()] ) ); updateListAdapter( m_phoneRecs );
tryEnable(); tryEnable();
} }

View file

@ -34,7 +34,6 @@ import android.telephony.PhoneNumberUtils;
import android.telephony.SmsManager; import android.telephony.SmsManager;
import android.telephony.TelephonyManager; import android.telephony.TelephonyManager;
import junit.framework.Assert;
import org.eehouse.android.xw4.MultiService.DictFetchOwner; import org.eehouse.android.xw4.MultiService.DictFetchOwner;
import org.eehouse.android.xw4.MultiService.MultiEvent; import org.eehouse.android.xw4.MultiService.MultiEvent;
@ -87,6 +86,7 @@ public class SMSService extends XWService {
private BroadcastReceiver m_sentReceiver; private BroadcastReceiver m_sentReceiver;
private BroadcastReceiver m_receiveReceiver; private BroadcastReceiver m_receiveReceiver;
private OnSharedPreferenceChangeListener m_prefsListener; private OnSharedPreferenceChangeListener m_prefsListener;
private SMSServiceHelper mHelper;
private int m_nReceived = 0; private int m_nReceived = 0;
private static int s_nSent = 0; private static int s_nSent = 0;
@ -175,28 +175,28 @@ public class SMSService extends XWService {
public static void stopService( Context context ) public static void stopService( Context context )
{ {
Intent intent = getIntentTo( context, SMSAction.STOP_SELF ); Intent intent = getIntentTo( context, SMSAction.STOP_SELF );
context.startService( intent ); startService( context, intent );
} }
// NBS case // NBS case
public static void handleFrom( Context context, byte[] buffer, public static void handleFrom( Context context, byte[] buffer,
String phone ) String phone )
{ {
Intent intent = getIntentTo( context, SMSAction.HANDLEDATA ); Intent intent = getIntentTo( context, SMSAction.HANDLEDATA )
intent.putExtra( BUFFER, buffer ); .putExtra( BUFFER, buffer )
intent.putExtra( PHONE, phone ); .putExtra( PHONE, phone );
context.startService( intent ); startService( context, intent );
} }
public static void inviteRemote( Context context, String phone, public static void inviteRemote( Context context, String phone,
NetLaunchInfo nli ) NetLaunchInfo nli )
{ {
Intent intent = getIntentTo( context, SMSAction.INVITE );
intent.putExtra( PHONE, phone );
Log.w( TAG, "inviteRemote(%s, '%s')", phone, nli ); Log.w( TAG, "inviteRemote(%s, '%s')", phone, nli );
byte[] data = nli.asByteArray(); byte[] data = nli.asByteArray();
intent.putExtra( GAMEDATA_BA, data ); Intent intent = getIntentTo( context, SMSAction.INVITE )
context.startService( intent ); .putExtra( PHONE, phone )
.putExtra( GAMEDATA_BA, data );
startService( context, intent );
} }
public static int sendPacket( Context context, String phone, public static int sendPacket( Context context, String phone,
@ -204,11 +204,11 @@ public class SMSService extends XWService {
{ {
int nSent = -1; int nSent = -1;
if ( XWPrefs.getSMSEnabled( context ) ) { if ( XWPrefs.getSMSEnabled( context ) ) {
Intent intent = getIntentTo( context, SMSAction.SEND ); Intent intent = getIntentTo( context, SMSAction.SEND )
intent.putExtra( PHONE, phone ); .putExtra( PHONE, phone )
intent.putExtra( MultiService.GAMEID, gameID ); .putExtra( MultiService.GAMEID, gameID )
intent.putExtra( BINBUFFER, binmsg ); .putExtra( BINBUFFER, binmsg );
context.startService( intent ); startService( context, intent );
nSent = binmsg.length; nSent = binmsg.length;
} else { } else {
Log.i( TAG, "sendPacket: dropping because SMS disabled" ); Log.i( TAG, "sendPacket: dropping because SMS disabled" );
@ -218,17 +218,17 @@ public class SMSService extends XWService {
public static void gameDied( Context context, int gameID, String phone ) public static void gameDied( Context context, int gameID, String phone )
{ {
Intent intent = getIntentTo( context, SMSAction.REMOVE ); Intent intent = getIntentTo( context, SMSAction.REMOVE )
intent.putExtra( PHONE, phone ); .putExtra( PHONE, phone )
intent.putExtra( MultiService.GAMEID, gameID ); .putExtra( MultiService.GAMEID, gameID );
context.startService( intent ); startService( context, intent );
} }
public static void onGameDictDownload( Context context, Intent intentOld ) public static void onGameDictDownload( Context context, Intent intentOld )
{ {
Intent intent = getIntentTo( context, SMSAction.ADDED_MISSING ); Intent intent = getIntentTo( context, SMSAction.ADDED_MISSING );
intent.fillIn( intentOld, 0 ); intent.fillIn( intentOld, 0 );
context.startService( intent ); startService( context, intent );
} }
public static String fromPublicFmt( String msg ) public static String fromPublicFmt( String msg )
@ -256,10 +256,16 @@ public class SMSService extends XWService {
return result; return result;
} }
private static void startService( Context context, Intent intent )
{
Log.d( TAG, "startService(%s)", intent );
context.startService( intent );
}
private static Intent getIntentTo( Context context, SMSAction cmd ) private static Intent getIntentTo( Context context, SMSAction cmd )
{ {
Intent intent = new Intent( context, SMSService.class ); Intent intent = new Intent( context, SMSService.class )
intent.putExtra( CMD_STR, cmd.ordinal() ); .putExtra( CMD_STR, cmd.ordinal() );
return intent; return intent;
} }
@ -272,15 +278,10 @@ public class SMSService extends XWService {
return s_showToasts; return s_showToasts;
} }
@Override
protected MultiMsgSink getSink( long rowid )
{
return new SMSMsgSink( this );
}
@Override @Override
public void onCreate() public void onCreate()
{ {
mHelper = new SMSServiceHelper( this );
if ( Utils.deviceSupportsSMS( this ) ) { if ( Utils.deviceSupportsSMS( this ) ) {
registerReceivers(); registerReceivers();
} else { } else {
@ -312,6 +313,7 @@ public class SMSService extends XWService {
@Override @Override
public int onStartCommand( Intent intent, int flags, int startId ) public int onStartCommand( Intent intent, int flags, int startId )
{ {
// Log.d( TAG, "onStartCommand(%s)", intent );
int result = Service.START_NOT_STICKY; int result = Service.START_NOT_STICKY;
if ( null != intent ) { if ( null != intent ) {
int ordinal = intent.getIntExtra( CMD_STR, -1 ); int ordinal = intent.getIntExtra( CMD_STR, -1 );
@ -463,10 +465,10 @@ public class SMSService extends XWService {
} }
break; break;
case DEATH: case DEATH:
postEvent( MultiEvent.MESSAGE_NOGAME, msg.gameID ); mHelper.postEvent( MultiEvent.MESSAGE_NOGAME, msg.gameID );
break; break;
case ACK_INVITE: case ACK_INVITE:
postEvent( MultiEvent.NEWGAME_SUCCESS, msg.gameID ); mHelper.postEvent( MultiEvent.NEWGAME_SUCCESS, msg.gameID );
break; break;
default: default:
Log.w( TAG, "unexpected cmd %s", msg.cmd ); Log.w( TAG, "unexpected cmd %s", msg.cmd );
@ -483,26 +485,17 @@ public class SMSService extends XWService {
for ( SMSProtoMsg msg : msgs ) { for ( SMSProtoMsg msg : msgs ) {
receive( msg, senderPhone ); receive( msg, senderPhone );
} }
postEvent( MultiEvent.SMS_RECEIVE_OK ); mHelper.postEvent( MultiEvent.SMS_RECEIVE_OK );
} else { } else {
Log.d( TAG, "receiveBuffer(): bogus or incomplete message from %s", Log.d( TAG, "receiveBuffer(): bogus or incomplete message from %s",
senderPhone ); senderPhone );
} }
} }
@Override
protected void postNotification( String phone, int gameID, long rowid )
{
String owner = Utils.phoneToContact( this, phone, true );
String body = LocUtils.getString( this, R.string.new_name_body_fmt,
owner );
GameUtils.postInvitedNotification( this, gameID, body, rowid );
}
private void makeForInvite( String phone, NetLaunchInfo nli ) private void makeForInvite( String phone, NetLaunchInfo nli )
{ {
if ( nli != null ) { if ( nli != null ) {
handleInvitation( nli, phone, DictFetchOwner.OWNER_SMS ); mHelper.handleInvitation( nli, phone, DictFetchOwner.OWNER_SMS );
ackInvite( phone, nli.gameID() ); ackInvite( phone, nli.gameID() );
} }
} }
@ -548,7 +541,7 @@ public class SMSService extends XWService {
} catch ( NullPointerException npe ) { } catch ( NullPointerException npe ) {
Assert.fail(); // shouldn't be trying to do this!!! Assert.fail(); // shouldn't be trying to do this!!!
} catch ( java.lang.SecurityException se ) { } catch ( java.lang.SecurityException se ) {
postEvent( MultiEvent.SMS_SEND_FAILED_NOPERMISSION ); mHelper.postEvent( MultiEvent.SMS_SEND_FAILED_NOPERMISSION );
} catch ( Exception ee ) { } catch ( Exception ee ) {
Log.ex( TAG, ee ); Log.ex( TAG, ee );
} }
@ -569,11 +562,12 @@ public class SMSService extends XWService {
private boolean feedMessage( int gameID, byte[] msg, CommsAddrRec addr ) private boolean feedMessage( int gameID, byte[] msg, CommsAddrRec addr )
{ {
ReceiveResult rslt = receiveMessage( this, gameID, null, msg, addr ); XWServiceHelper.ReceiveResult rslt = mHelper
if ( ReceiveResult.GAME_GONE == rslt ) { .receiveMessage( this, gameID, null, msg, addr );
if ( XWServiceHelper.ReceiveResult.GAME_GONE == rslt ) {
sendDiedPacket( addr.sms_phone, gameID ); sendDiedPacket( addr.sms_phone, gameID );
} }
return rslt == ReceiveResult.OK; return rslt == XWServiceHelper.ReceiveResult.OK;
} }
private void registerReceivers() private void registerReceivers()
@ -584,15 +578,15 @@ public class SMSService extends XWService {
{ {
switch ( getResultCode() ) { switch ( getResultCode() ) {
case Activity.RESULT_OK: case Activity.RESULT_OK:
postEvent( MultiEvent.SMS_SEND_OK ); mHelper.postEvent( MultiEvent.SMS_SEND_OK );
break; break;
case SmsManager.RESULT_ERROR_RADIO_OFF: case SmsManager.RESULT_ERROR_RADIO_OFF:
postEvent( MultiEvent.SMS_SEND_FAILED_NORADIO ); mHelper.postEvent( MultiEvent.SMS_SEND_FAILED_NORADIO );
break; break;
case SmsManager.RESULT_ERROR_NO_SERVICE: case SmsManager.RESULT_ERROR_NO_SERVICE:
default: default:
Log.w( TAG, "FAILURE!!!" ); Log.w( TAG, "FAILURE!!!" );
postEvent( MultiEvent.SMS_SEND_FAILED ); mHelper.postEvent( MultiEvent.SMS_SEND_FAILED );
break; break;
} }
} }
@ -649,4 +643,28 @@ public class SMSService extends XWService {
return sendPacket( addr.sms_phone, gameID, buf ); return sendPacket( addr.sms_phone, gameID, buf );
} }
} }
private class SMSServiceHelper extends XWServiceHelper {
private Service mService;
SMSServiceHelper( Service service ) {
super( service );
mService = service;
}
@Override
protected MultiMsgSink getSink( long rowid )
{
return new SMSMsgSink( SMSService.this );
}
@Override
protected void postNotification( String phone, int gameID, long rowid )
{
String owner = Utils.phoneToContact( mService, phone, true );
String body = LocUtils.getString( mService, R.string.new_name_body_fmt,
owner );
GameUtils.postInvitedNotification( mService, gameID, body, rowid );
}
}
} }

View file

@ -36,7 +36,6 @@ import android.widget.ArrayAdapter;
import android.widget.ListView; import android.widget.ListView;
import android.widget.Spinner; import android.widget.Spinner;
import junit.framework.Assert;
import org.eehouse.android.xw4.DlgDelegate.Action; import org.eehouse.android.xw4.DlgDelegate.Action;
import org.eehouse.android.xw4.jni.GameSummary; import org.eehouse.android.xw4.jni.GameSummary;

View file

@ -31,7 +31,6 @@ import android.widget.Button;
import java.io.Serializable; import java.io.Serializable;
import junit.framework.Assert;
import org.eehouse.android.xw4.DlgDelegate.Action; import org.eehouse.android.xw4.DlgDelegate.Action;
import org.eehouse.android.xw4.DlgDelegate.DlgClickNotify; import org.eehouse.android.xw4.DlgDelegate.DlgClickNotify;

View file

@ -38,7 +38,6 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import junit.framework.Assert;
public class TilePickView extends LinearLayout { public class TilePickView extends LinearLayout {
private static final String TAG = TilePickView.class.getSimpleName(); private static final String TAG = TilePickView.class.getSimpleName();

View file

@ -35,7 +35,6 @@ import java.util.Iterator;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import junit.framework.Assert;
import org.eehouse.android.xw4.DlgDelegate.Action; import org.eehouse.android.xw4.DlgDelegate.Action;
import org.eehouse.android.xw4.DlgDelegate.HasDlgDelegate; import org.eehouse.android.xw4.DlgDelegate.HasDlgDelegate;

View file

@ -23,6 +23,7 @@ package org.eehouse.android.xw4;
import android.app.Activity; import android.app.Activity;
import android.app.Dialog; import android.app.Dialog;
import android.app.Notification; import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.ContentResolver; import android.content.ContentResolver;
@ -38,6 +39,8 @@ import android.database.Cursor;
import android.media.Ringtone; import android.media.Ringtone;
import android.media.RingtoneManager; import android.media.RingtoneManager;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Looper;
import android.provider.ContactsContract.PhoneLookup; import android.provider.ContactsContract.PhoneLookup;
import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat;
import android.support.v4.content.FileProvider; import android.support.v4.content.FileProvider;
@ -71,7 +74,6 @@ import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Random; import java.util.Random;
import junit.framework.Assert;
import org.eehouse.android.xw4.Perms23.Perm; import org.eehouse.android.xw4.Perms23.Perm;
import org.eehouse.android.xw4.jni.CommonPrefs; import org.eehouse.android.xw4.jni.CommonPrefs;
@ -254,7 +256,9 @@ public class Utils {
defaults |= Notification.DEFAULT_VIBRATE; defaults |= Notification.DEFAULT_VIBRATE;
} }
Notification notification = new NotificationCompat.Builder( context ) String channelID = Channels.getChannelID( context, Channels.ID.GAME_EVENT );
Notification notification =
new NotificationCompat.Builder( context, channelID )
.setContentIntent( pi ) .setContentIntent( pi )
.setSmallIcon( R.drawable.notify ) .setSmallIcon( R.drawable.notify )
//.setTicker(body) //.setTicker(body)
@ -526,6 +530,11 @@ public class Utils {
return result; return result;
} }
public static boolean isOnUIThread()
{
return Looper.getMainLooper().equals(Looper.myLooper());
}
public static String base64Encode( byte[] in ) public static String base64Encode( byte[] in )
{ {
return Base64.encodeToString( in, Base64.NO_WRAP ); return Base64.encodeToString( in, Base64.NO_WRAP );
@ -568,7 +577,7 @@ public class Utils {
public static void testSerialization( Serializable obj ) public static void testSerialization( Serializable obj )
{ {
if ( BuildConfig.DEBUG ) { if ( false && BuildConfig.DEBUG ) {
String as64 = serializableToString64( obj ); String as64 = serializableToString64( obj );
Object other = string64ToSerializable( as64 ); Object other = string64ToSerializable( as64 );
Assert.assertTrue( other.equals( obj ) ); Assert.assertTrue( other.equals( obj ) );
@ -610,5 +619,4 @@ public class Utils {
} }
} }
} }
} }

View file

@ -27,10 +27,11 @@ import android.os.Bundle;
import android.view.View; import android.view.View;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.TextView; import android.widget.TextView;
import java.util.Iterator;
import java.util.Map;
import junit.framework.Assert; import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
public class WiDirInviteDelegate extends InviteDelegate public class WiDirInviteDelegate extends InviteDelegate
implements WiDirService.DevSetListener { implements WiDirService.DevSetListener {
@ -76,11 +77,17 @@ public class WiDirInviteDelegate extends InviteDelegate
WiDirService.unregisterDevSetListener( this ); WiDirService.unregisterDevSetListener( this );
} }
protected void onBarButtonClicked( int id )
{
// not implemented yet as there's no bar button
Assert.assertFalse( BuildConfig.DEBUG );
}
@Override @Override
protected void onChildAdded( View child, InviterItem data ) protected void onChildAdded( View child, InviterItem data )
{ {
TwoStringPair pair = (TwoStringPair)data; TwoStringPair pair = (TwoStringPair)data;
((TwoStrsItem)child).setStrings( pair.str2, pair.str1 ); ((TwoStrsItem)child).setStrings( pair.str2, pair.getDev() );
} }
@Override @Override
@ -88,7 +95,7 @@ public class WiDirInviteDelegate extends InviteDelegate
{ {
for ( int ii = 0; ii < selected.length; ++ii ) { for ( int ii = 0; ii < selected.length; ++ii ) {
TwoStringPair pair = (TwoStringPair)selected[ii]; TwoStringPair pair = (TwoStringPair)selected[ii];
devs[ii] = pair.str1; devs[ii] = pair.getDev();
} }
} }
@ -107,13 +114,13 @@ public class WiDirInviteDelegate extends InviteDelegate
private void rebuildList() private void rebuildList()
{ {
int count = m_macsToName.size(); int count = m_macsToName.size();
TwoStringPair[] pairs = new TwoStringPair[count]; List<TwoStringPair> pairs = new ArrayList<>();
// String[] names = new String[count]; // String[] names = new String[count];
// String[] addrs = new String[count]; // String[] addrs = new String[count];
Iterator<String> iter = m_macsToName.keySet().iterator(); Iterator<String> iter = m_macsToName.keySet().iterator();
for ( int ii = 0; ii < count; ++ii ) { for ( int ii = 0; ii < count; ++ii ) {
String mac = iter.next(); String mac = iter.next();
pairs[ii] = new TwoStringPair(mac, m_macsToName.get(mac) ); pairs.add( new TwoStringPair(mac, m_macsToName.get(mac) ) );
// addrs[ii] = mac; // addrs[ii] = mac;
// names[ii] = m_macsToName.get(mac); // names[ii] = m_macsToName.get(mac);
} }

View file

@ -69,7 +69,6 @@ import org.eehouse.android.xw4.jni.CommsAddrRec;
import org.eehouse.android.xw4.jni.XwJNI; import org.eehouse.android.xw4.jni.XwJNI;
import org.eehouse.android.xw4.loc.LocUtils; import org.eehouse.android.xw4.loc.LocUtils;
import junit.framework.Assert;
public class WiDirService extends XWService { public class WiDirService extends XWService {
private static final String TAG = WiDirService.class.getSimpleName(); private static final String TAG = WiDirService.class.getSimpleName();
@ -125,6 +124,7 @@ public class WiDirService extends XWService {
private static Set<String> s_peersSet; private static Set<String> s_peersSet;
private P2pMsgSink m_sink; private P2pMsgSink m_sink;
private WiDirServiceHelper mHelper;
public interface DevSetListener { public interface DevSetListener {
void setChanged( Map<String, String> macToName ); void setChanged( Map<String, String> macToName );
@ -134,6 +134,7 @@ public class WiDirService extends XWService {
public void onCreate() public void onCreate()
{ {
m_sink = new P2pMsgSink(); m_sink = new P2pMsgSink();
mHelper = new WiDirServiceHelper(this);
} }
@Override @Override
@ -214,6 +215,8 @@ public class WiDirService extends XWService {
sHavePermission = false; sHavePermission = false;
} catch ( SecurityException se ) { // perm not in manifest } catch ( SecurityException se ) { // perm not in manifest
sHavePermission = false; sHavePermission = false;
} catch ( NullPointerException npe ) { // Seeing this on Oreo emulator
sHavePermission = false;
} }
} }
@ -750,8 +753,9 @@ public class WiDirService extends XWService {
CommsAddrRec addr = new CommsAddrRec( CommsConnType.COMMS_CONN_P2P ) CommsAddrRec addr = new CommsAddrRec( CommsConnType.COMMS_CONN_P2P )
.setP2PParams( macAddress ); .setP2PParams( macAddress );
ReceiveResult rslt = receiveMessage( this, gameID, m_sink, data, addr ); XWServiceHelper.ReceiveResult rslt = mHelper
if ( ReceiveResult.GAME_GONE == rslt ) { .receiveMessage( this, gameID, m_sink, data, addr );
if ( XWServiceHelper.ReceiveResult.GAME_GONE == rslt ) {
sendNoGame( null, macAddress, gameID ); sendNoGame( null, macAddress, gameID );
} }
} }
@ -763,27 +767,15 @@ public class WiDirService extends XWService {
NetLaunchInfo nli = NetLaunchInfo.makeFrom( this, nliData ); NetLaunchInfo nli = NetLaunchInfo.makeFrom( this, nliData );
String returnMac = intent.getStringExtra( KEY_SRC ); String returnMac = intent.getStringExtra( KEY_SRC );
if ( !handleInvitation( nli, returnMac, DictFetchOwner.OWNER_P2P ) ) { if ( !mHelper.handleInvitation( nli, returnMac, DictFetchOwner.OWNER_P2P ) ) {
Log.d( TAG, "handleInvitation() failed" ); Log.d( TAG, "handleInvitation() failed" );
} }
} }
@Override
void postNotification( String device, int gameID, long rowid )
{
Log.e( TAG, "postNotification() doing nothing" );
}
@Override
MultiMsgSink getSink( long rowid )
{
return m_sink;
}
private void handleGameGone( Intent intent ) private void handleGameGone( Intent intent )
{ {
int gameID = intent.getIntExtra( KEY_GAMEID, 0 ); int gameID = intent.getIntExtra( KEY_GAMEID, 0 );
postEvent( MultiEvent.MESSAGE_NOGAME, gameID ); mHelper.postEvent( MultiEvent.MESSAGE_NOGAME, gameID );
} }
private void makeGame( NetLaunchInfo nli, String senderMac ) private void makeGame( NetLaunchInfo nli, String senderMac )
@ -793,7 +785,7 @@ public class WiDirService extends XWService {
CommsAddrRec addr = nli.makeAddrRec( this ); CommsAddrRec addr = nli.makeAddrRec( this );
long rowid = GameUtils.makeNewMultiGame( this, nli, long rowid = GameUtils.makeNewMultiGame( this, nli,
m_sink, m_sink,
getUtilCtxt() ); mHelper.getUtilCtxt() );
if ( DBUtils.ROWID_NOTFOUND != rowid ) { if ( DBUtils.ROWID_NOTFOUND != rowid ) {
if ( null != nli.gameName && 0 < nli.gameName.length() ) { if ( null != nli.gameName && 0 < nli.gameName.length() ) {
DBUtils.setName( this, rowid, nli.gameName ); DBUtils.setName( this, rowid, nli.gameName );
@ -1205,4 +1197,23 @@ public class WiDirService extends XWService {
private class P2pMsgSink extends MultiMsgSink { private class P2pMsgSink extends MultiMsgSink {
public P2pMsgSink() { super( WiDirService.this ); } public P2pMsgSink() { super( WiDirService.this ); }
} }
private class WiDirServiceHelper extends XWServiceHelper {
WiDirServiceHelper( Service service ) {
super( service );
}
@Override
MultiMsgSink getSink( long rowid )
{
return m_sink;
}
@Override
void postNotification( String device, int gameID, long rowid )
{
Log.e( TAG, "postNotification() doing nothing" );
}
}
} }

View file

@ -40,7 +40,6 @@ import android.widget.ListView;
import org.eehouse.android.xw4.DlgDelegate.Action; import org.eehouse.android.xw4.DlgDelegate.Action;
import junit.framework.Assert;
public class XWActivity extends FragmentActivity public class XWActivity extends FragmentActivity
implements Delegator, DlgDelegate.DlgClickNotify { implements Delegator, DlgDelegate.DlgClickNotify {

View file

@ -21,6 +21,11 @@
package org.eehouse.android.xw4; package org.eehouse.android.xw4;
import android.app.Application; import android.app.Application;
import android.arch.lifecycle.Lifecycle;
import android.arch.lifecycle.LifecycleObserver;
import android.arch.lifecycle.LifecycleOwner;
import android.arch.lifecycle.OnLifecycleEvent;
import android.arch.lifecycle.ProcessLifecycleOwner;
import android.content.Context; import android.content.Context;
import android.graphics.Color; import android.graphics.Color;
import android.os.Build; import android.os.Build;
@ -30,9 +35,10 @@ import org.eehouse.android.xw4.jni.XwJNI;
import java.util.UUID; import java.util.UUID;
import junit.framework.Assert;
public class XWApp extends Application { import static android.arch.lifecycle.Lifecycle.Event.ON_ANY;
public class XWApp extends Application implements LifecycleObserver {
private static final String TAG = XWApp.class.getSimpleName(); private static final String TAG = XWApp.class.getSimpleName();
public static final boolean BTSUPPORTED = true; public static final boolean BTSUPPORTED = true;
@ -44,8 +50,6 @@ public class XWApp extends Application {
public static final boolean LOCUTILS_ENABLED = false; public static final boolean LOCUTILS_ENABLED = false;
public static final boolean CONTEXT_MENUS_ENABLED = true; public static final boolean CONTEXT_MENUS_ENABLED = true;
public static final boolean OFFER_DUALPANE = false; public static final boolean OFFER_DUALPANE = false;
// BT class "COMPUTERS" includes tablets like the Nexus 9
public static final boolean BT_SCAN_COMPUTERS = true;
public static final String SMS_PUBLIC_HEADER = "-XW4"; public static final String SMS_PUBLIC_HEADER = "-XW4";
public static final int MAX_TRAY_TILES = 7; // comtypes.h public static final int MAX_TRAY_TILES = 7; // comtypes.h
@ -65,6 +69,8 @@ public class XWApp extends Application {
Assert.assertTrue( s_context == s_context.getApplicationContext() ); Assert.assertTrue( s_context == s_context.getApplicationContext() );
super.onCreate(); super.onCreate();
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
// This one line should always get logged even if logging is // This one line should always get logged even if logging is
// off -- because logging is on by default until logEnable is // off -- because logging is on by default until logEnable is
// called. // called.
@ -85,12 +91,25 @@ public class XWApp extends Application {
} }
UpdateCheckReceiver.restartTimer( this ); UpdateCheckReceiver.restartTimer( this );
BTService.startService( this );
RelayService.startService( this ); RelayService.startService( this );
GCMIntentService.init( this ); GCMIntentService.init( this );
WiDirWrapper.init( this ); WiDirWrapper.init( this );
} }
@OnLifecycleEvent(ON_ANY)
public void onAny( LifecycleOwner source, Lifecycle.Event event )
{
Log.d( TAG, "onAny(%s)", event );
switch( event ) {
case ON_RESUME:
BTService.onAppToForeground( this );
break;
case ON_STOP:
BTService.onAppToBackground( this );
break;
}
}
// This is called on emulator only, but good for ensuring no memory leaks // This is called on emulator only, but good for ensuring no memory leaks
// by forcing JNI cleanup // by forcing JNI cleanup
@Override @Override

View file

@ -27,7 +27,6 @@ import android.preference.DialogPreference;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.View; import android.view.View;
import junit.framework.Assert;
import org.eehouse.android.xw4.DlgDelegate.Action; import org.eehouse.android.xw4.DlgDelegate.Action;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType; import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType;

View file

@ -30,7 +30,6 @@ import android.view.View;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import junit.framework.Assert;
abstract class XWDialogFragment extends DialogFragment { abstract class XWDialogFragment extends DialogFragment {
private static final String TAG = XWDialogFragment.class.getSimpleName(); private static final String TAG = XWDialogFragment.class.getSimpleName();

View file

@ -23,7 +23,6 @@ package org.eehouse.android.xw4;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import junit.framework.Assert;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;

View file

@ -36,7 +36,6 @@ import android.widget.ListView;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import junit.framework.Assert;
abstract class XWFragment extends Fragment implements Delegator { abstract class XWFragment extends Fragment implements Delegator {
private static final String TAG = XWFragment.class.getSimpleName(); private static final String TAG = XWFragment.class.getSimpleName();

View file

@ -29,7 +29,6 @@ import android.text.TextUtils;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import junit.framework.Assert;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType; import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnTypeSet; import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnTypeSet;

View file

@ -1,6 +1,6 @@
/* -*- compile-command: "find-and-gradle.sh insXw4Deb"; -*- */ /* -*- compile-command: "find-and-gradle.sh insXw4Deb"; -*- */
/* /*
* Copyright 2010 - 2012 by Eric House (xwords@eehouse.org). All * Copyright 2010 - 2018 by Eric House (xwords@eehouse.org). All
* rights reserved. * rights reserved.
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -21,165 +21,15 @@
package org.eehouse.android.xw4; package org.eehouse.android.xw4;
import android.app.Service; import android.app.Service;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.IBinder; import android.os.IBinder;
import junit.framework.Assert; class XWService extends Service {
import org.eehouse.android.xw4.MultiService.DictFetchOwner;
import org.eehouse.android.xw4.MultiService.MultiEvent;
import org.eehouse.android.xw4.jni.CommsAddrRec;
import org.eehouse.android.xw4.jni.JNIThread;
import org.eehouse.android.xw4.jni.UtilCtxt;
import org.eehouse.android.xw4.jni.UtilCtxtImpl;
import java.util.HashSet;
import java.util.Set;
abstract class XWService extends Service {
private static final String TAG = XWService.class.getSimpleName(); private static final String TAG = XWService.class.getSimpleName();
public static enum ReceiveResult { OK, GAME_GONE, UNCONSUMED };
protected static MultiService s_srcMgr = null;
private static Set<String> s_seen = new HashSet<String>();
private UtilCtxt m_utilCtxt;
@Override @Override
public IBinder onBind( Intent intent ) public IBinder onBind( Intent intent )
{ {
return null; return null;
} }
public final static void setListener( MultiService.MultiEventListener li )
{
if ( null == s_srcMgr ) {
// DbgUtils.logf( "XWService.setListener: registering %s", li.getClass().getName() );
s_srcMgr = new MultiService();
}
s_srcMgr.setListener( li );
}
protected void postEvent( MultiEvent event, Object ... args )
{
if ( null != s_srcMgr ) {
s_srcMgr.postEvent( event, args );
} else {
Log.d( TAG, "postEvent(): dropping %s event",
event.toString() );
}
}
// Check that we aren't already processing an invitation with this
// inviteID.
private boolean checkNotDupe( NetLaunchInfo nli )
{
String inviteID = nli.inviteID();
boolean isDupe;
synchronized( s_seen ) {
isDupe = s_seen.contains( inviteID );
if ( !isDupe ) {
s_seen.add( inviteID );
}
}
Log.d( TAG, "checkNotDupe('%s') => %b", inviteID, !isDupe );
return !isDupe;
}
abstract void postNotification( String device, int gameID, long rowid );
// Return true if able to start game only
protected boolean handleInvitation( NetLaunchInfo nli, String device,
DictFetchOwner dfo )
{
boolean success = false;
long[] rowids = DBUtils.getRowIDsFor( this, nli.gameID() );
if ( 0 == rowids.length
|| ( rowids.length < nli.nPlayersT // will break for two-per-device game
&& XWPrefs.getSecondInviteAllowed( this ) ) ) {
if ( nli.isValid() && checkNotDupe( nli ) ) {
if ( DictLangCache.haveDict( this, nli.lang, nli.dict ) ) {
long rowid = GameUtils.makeNewMultiGame( this, nli,
getSink( 0 ),
getUtilCtxt() );
if ( null != nli.gameName && 0 < nli.gameName.length() ) {
DBUtils.setName( this, rowid, nli.gameName );
}
postNotification( device, nli.gameID(), rowid );
success = true;
} else {
Intent intent = MultiService
.makeMissingDictIntent( this, nli, dfo );
MultiService.postMissingDictNotification( this, intent,
nli.gameID() );
}
}
}
Log.d( TAG, "handleInvitation() => %b", success );
return success;
}
protected UtilCtxt getUtilCtxt()
{
if ( null == m_utilCtxt ) {
m_utilCtxt = new UtilCtxtImpl( this );
}
return m_utilCtxt;
}
// Meant to be overridden
abstract MultiMsgSink getSink( long rowid );
protected ReceiveResult receiveMessage( Context context, int gameID,
MultiMsgSink sink, byte[] msg,
CommsAddrRec addr )
{
ReceiveResult result;
long[] rowids = DBUtils.getRowIDsFor( context, gameID );
if ( null == rowids || 0 == rowids.length ) {
result = ReceiveResult.GAME_GONE;
} else {
result = ReceiveResult.UNCONSUMED;
for ( long rowid : rowids ) {
if ( receiveMessage( context, rowid, sink, msg, addr ) ) {
result = ReceiveResult.OK;
}
}
}
return result;
}
protected boolean receiveMessage( Context context, long rowid,
MultiMsgSink sink, byte[] msg,
CommsAddrRec addr )
{
boolean allConsumed = true;
boolean[] isLocalP = new boolean[1];
JNIThread jniThread = JNIThread.getRetained( rowid, false );
boolean consumed = false;
if ( null != jniThread ) {
consumed = true;
jniThread.receive( msg, addr ).release();
} else {
GameUtils.BackMoveResult bmr = new GameUtils.BackMoveResult();
if ( null == sink ) {
sink = getSink( rowid );
}
if ( GameUtils.feedMessage( context, rowid, msg, addr,
sink, bmr, isLocalP ) ) {
consumed = true;
GameUtils.postMoveNotification( context, rowid, bmr,
isLocalP[0] );
}
}
if ( allConsumed && !consumed ) {
allConsumed = false;
}
return allConsumed;
}
} }

View file

@ -0,0 +1,215 @@
/* -*- compile-command: "find-and-gradle.sh insXw4Deb"; -*- */
/*
* Copyright 2010 - 2018 by Eric House (xwords@eehouse.org). All
* rights reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This program 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
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package org.eehouse.android.xw4;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import org.eehouse.android.xw4.MultiService.DictFetchOwner;
import org.eehouse.android.xw4.MultiService.MultiEvent;
import org.eehouse.android.xw4.jni.CommsAddrRec;
import org.eehouse.android.xw4.jni.CurGameInfo;
import org.eehouse.android.xw4.jni.JNIThread;
import org.eehouse.android.xw4.jni.UtilCtxt;
import org.eehouse.android.xw4.jni.UtilCtxtImpl;
import org.eehouse.android.xw4.jni.XwJNI.GamePtr;
import java.util.HashMap;
import java.util.Map;
abstract class XWServiceHelper {
private static final String TAG = XWServiceHelper.class.getSimpleName();
private Service mService;
private static MultiService s_srcMgr = null;
public static enum ReceiveResult { OK, GAME_GONE, UNCONSUMED };
XWServiceHelper( Service service )
{
mService = service;
}
abstract MultiMsgSink getSink( long rowid );
abstract void postNotification( String device, int gameID, long rowid );
protected ReceiveResult receiveMessage( Context context, int gameID,
MultiMsgSink sink, byte[] msg,
CommsAddrRec addr )
{
ReceiveResult result;
long[] rowids = DBUtils.getRowIDsFor( context, gameID );
if ( null == rowids || 0 == rowids.length ) {
result = ReceiveResult.GAME_GONE;
} else {
result = ReceiveResult.UNCONSUMED;
for ( long rowid : rowids ) {
if ( receiveMessage( context, rowid, sink, msg, addr ) ) {
result = ReceiveResult.OK;
}
}
}
return result;
}
protected boolean receiveMessage( Context context, long rowid,
MultiMsgSink sink, byte[] msg,
CommsAddrRec addr )
{
boolean allConsumed = true;
boolean[] isLocalP = new boolean[1];
JNIThread jniThread = JNIThread.getRetained( context, rowid );
boolean consumed = false;
if ( null != jniThread ) {
consumed = true;
jniThread.receive( msg, addr ).release();
} else {
GameUtils.BackMoveResult bmr = new GameUtils.BackMoveResult();
if ( null == sink ) {
sink = getSink( rowid );
}
if ( GameUtils.feedMessage( context, rowid, msg, addr,
sink, bmr, isLocalP ) ) {
consumed = true;
GameUtils.postMoveNotification( context, rowid, bmr,
isLocalP[0] );
}
}
if ( allConsumed && !consumed ) {
allConsumed = false;
}
return allConsumed;
}
public final static void setListener( MultiService.MultiEventListener li )
{
if ( null == s_srcMgr ) {
// DbgUtils.logf( "XWService.setListener: registering %s",
// li.getClass().getName() );
s_srcMgr = new MultiService();
}
s_srcMgr.setListener( li );
}
protected void postEvent( MultiEvent event, Object ... args )
{
if ( null != s_srcMgr ) {
s_srcMgr.postEvent( event, args );
} else {
Log.d( TAG, "postEvent(): dropping %s event",
event.toString() );
}
}
protected boolean handleInvitation( NetLaunchInfo nli, String device,
DictFetchOwner dfo )
{
boolean success = nli.isValid() && checkNotInFlight( nli );
if ( success ) {
long[] rowids = DBUtils.getRowIDsFor( mService, nli.gameID() );
if ( 0 == rowids.length ) {
// cool: we're good
} else if ( rowids.length < nli.nPlayersT ) {
success = XWPrefs.getSecondInviteAllowed( mService );
// Allowing a second game allows the common testing action of
// sending invitation to myself. But we still need to check
// for duplicates! forceChannel's hard to dig up, but works
for ( int ii = 0; success && ii < rowids.length; ++ii ) {
long rowid = rowids[ii];
CurGameInfo gi = null;
try ( GameLock lock = GameLock.getFor( rowid ).tryLockRO() ) {
// drop invite if can't open game; likely a dupe!
if ( null != lock ) {
gi = new CurGameInfo( mService );
GamePtr gamePtr = GameUtils
.loadMakeGame( mService, gi, lock );
gamePtr.release();
}
}
if ( null == gi ) {
// locked. Maybe it's open?
JNIThread thread = JNIThread.getRetained( mService, rowid );
if ( null != thread ) {
gi = thread.getGI();
thread.release( false );
}
}
success = null != gi && gi.forceChannel != nli.forceChannel;
}
} else {
success = false;
}
if ( success ) {
if ( DictLangCache.haveDict( mService, nli.lang, nli.dict ) ) {
long rowid = GameUtils.makeNewMultiGame( mService, nli,
getSink( 0 ),
getUtilCtxt() );
if ( null != nli.gameName && 0 < nli.gameName.length() ) {
DBUtils.setName( mService, rowid, nli.gameName );
}
postNotification( device, nli.gameID(), rowid );
} else {
Intent intent = MultiService
.makeMissingDictIntent( mService, nli, dfo );
MultiService.postMissingDictNotification( mService, intent,
nli.gameID() );
success = false;
}
}
}
Log.d( TAG, "handleInvitation() => %b", success );
return success;
}
private UtilCtxt m_utilCtxt;
protected UtilCtxt getUtilCtxt()
{
if ( null == m_utilCtxt ) {
m_utilCtxt = new UtilCtxtImpl( mService );
}
return m_utilCtxt;
}
// Check that we aren't already processing an invitation with this
// inviteID.
private static final long SEEN_INTERVAL_MS = 1000 * 2;
private static Map<String, Long> s_seen = new HashMap<>();
private boolean checkNotInFlight( NetLaunchInfo nli )
{
boolean inProcess;
String inviteID = nli.inviteID();
synchronized( s_seen ) {
long now = System.currentTimeMillis();
Long lastSeen = s_seen.get( inviteID );
inProcess = null != lastSeen && lastSeen + SEEN_INTERVAL_MS > now;
if ( !inProcess ) {
s_seen.put( inviteID, now );
}
}
Log.d( TAG, "checkNotInFlight('%s') => %b", inviteID, !inProcess );
return !inProcess;
}
}

View file

@ -23,8 +23,7 @@ package org.eehouse.android.xw4.jni;
import android.content.Context; import android.content.Context;
import android.text.TextUtils; import android.text.TextUtils;
import junit.framework.Assert; import org.eehouse.android.xw4.Assert;
import org.eehouse.android.xw4.BTService; import org.eehouse.android.xw4.BTService;
import org.eehouse.android.xw4.GameUtils; import org.eehouse.android.xw4.GameUtils;
import org.eehouse.android.xw4.Log; import org.eehouse.android.xw4.Log;

View file

@ -29,8 +29,8 @@ import java.util.HashSet;
import java.util.Random; import java.util.Random;
import org.json.JSONObject; import org.json.JSONObject;
import junit.framework.Assert;
import org.eehouse.android.xw4.Assert;
import org.eehouse.android.xw4.BuildConfig; import org.eehouse.android.xw4.BuildConfig;
import org.eehouse.android.xw4.DictLangCache; import org.eehouse.android.xw4.DictLangCache;
import org.eehouse.android.xw4.DictUtils; import org.eehouse.android.xw4.DictUtils;

View file

@ -23,8 +23,7 @@ package org.eehouse.android.xw4.jni;
import android.content.Context; import android.content.Context;
import android.telephony.PhoneNumberUtils; import android.telephony.PhoneNumberUtils;
import junit.framework.Assert; import org.eehouse.android.xw4.Assert;
import org.eehouse.android.xw4.XWApp; import org.eehouse.android.xw4.XWApp;
import org.eehouse.android.xw4.DBUtils; import org.eehouse.android.xw4.DBUtils;
import org.eehouse.android.xw4.DevID; import org.eehouse.android.xw4.DevID;

View file

@ -28,8 +28,7 @@ import java.util.Arrays;
import org.json.JSONObject; import org.json.JSONObject;
import junit.framework.Assert; import org.eehouse.android.xw4.Assert;
import org.eehouse.android.xw4.BuildConfig; import org.eehouse.android.xw4.BuildConfig;
import org.eehouse.android.xw4.DBUtils; import org.eehouse.android.xw4.DBUtils;
import org.eehouse.android.xw4.Log; import org.eehouse.android.xw4.Log;

View file

@ -26,8 +26,7 @@ import android.graphics.Bitmap;
import android.os.Handler; import android.os.Handler;
import android.os.Message; import android.os.Message;
import junit.framework.Assert; import org.eehouse.android.xw4.Assert;
import org.eehouse.android.xw4.BuildConfig; import org.eehouse.android.xw4.BuildConfig;
import org.eehouse.android.xw4.CommsTransport; import org.eehouse.android.xw4.CommsTransport;
import org.eehouse.android.xw4.ConnStatusHandler; import org.eehouse.android.xw4.ConnStatusHandler;
@ -36,6 +35,7 @@ import org.eehouse.android.xw4.DbgUtils;
import org.eehouse.android.xw4.DictUtils; import org.eehouse.android.xw4.DictUtils;
import org.eehouse.android.xw4.GameLock; import org.eehouse.android.xw4.GameLock;
import org.eehouse.android.xw4.GameUtils; import org.eehouse.android.xw4.GameUtils;
import org.eehouse.android.xw4.Utils;
import org.eehouse.android.xw4.Log; import org.eehouse.android.xw4.Log;
import org.eehouse.android.xw4.R; import org.eehouse.android.xw4.R;
import org.eehouse.android.xw4.XWPrefs; import org.eehouse.android.xw4.XWPrefs;
@ -217,23 +217,29 @@ public class JNIThread extends Thread {
if ( null != m_jniGamePtr ) { if ( null != m_jniGamePtr ) {
Log.d( TAG, "configure(): m_jniGamePtr not null; that ok?" ); Log.d( TAG, "configure(): m_jniGamePtr not null; that ok?" );
} }
m_jniGamePtr = null;
if ( null != stream ) { synchronized ( this ) {
m_jniGamePtr = XwJNI.initFromStream( m_rowid, stream, m_gi, m_jniGamePtr = null;
dictNames, pairs.m_bytes, if ( null != stream ) {
pairs.m_paths, m_jniGamePtr = XwJNI.initFromStream( m_rowid, stream, m_gi,
m_gi.langName( m_context ), dictNames, pairs.m_bytes,
utils, null, cp, m_xport ); pairs.m_paths,
m_gi.langName( m_context ),
utils, null, cp, m_xport );
}
if ( null == m_jniGamePtr ) {
m_jniGamePtr = XwJNI.initNew( m_gi, dictNames, pairs.m_bytes,
pairs.m_paths,
m_gi.langName(m_context),
utils, null, cp, m_xport );
}
Assert.assertNotNull( m_jniGamePtr );
notifyAll();
} }
if ( null == m_jniGamePtr ) {
m_jniGamePtr = XwJNI.initNew( m_gi, dictNames, pairs.m_bytes,
pairs.m_paths,
m_gi.langName(m_context),
utils, null, cp, m_xport );
}
Assert.assertNotNull( m_jniGamePtr );
m_lastSavedState = Arrays.hashCode( stream ); m_lastSavedState = Arrays.hashCode( stream );
} }
Log.d( TAG, "configure() => %b", success );
return success; return success;
} }
@ -418,13 +424,24 @@ public class JNIThread extends Thread {
} }
@SuppressWarnings("fallthrough") @SuppressWarnings("fallthrough")
@Override
public void run() public void run()
{ {
Log.d( TAG, "run() starting" );
boolean[] barr = new boolean[2]; // scratch boolean boolean[] barr = new boolean[2]; // scratch boolean
for ( ; ; ) { for ( ; ; ) {
synchronized ( this ) { synchronized ( this ) {
if ( m_stopped ) { if ( m_stopped ) {
break; break;
} else if ( null == m_jniGamePtr ) {
try {
wait();
} catch ( InterruptedException iex ) {
Log.d( TAG, "exiting run() on interrupt: %s",
iex.getMessage() );
break;
}
continue;
} }
} }
@ -729,6 +746,7 @@ public class JNIThread extends Thread {
m_jniGamePtr.release(); m_jniGamePtr.release();
m_jniGamePtr = null; m_jniGamePtr = null;
} }
Log.d( TAG, "run() finished" );
} // run } // run
public void handleBkgrnd( JNICmd cmd, Object... args ) public void handleBkgrnd( JNICmd cmd, Object... args )
@ -751,8 +769,8 @@ public class JNIThread extends Thread {
public void handle( JNICmd cmd, Object... args ) public void handle( JNICmd cmd, Object... args )
{ {
if ( m_stopped && ! JNICmd.CMD_NONE.equals(cmd) ) { if ( m_stopped && ! JNICmd.CMD_NONE.equals(cmd) ) {
Log.w( TAG, "NOT adding %s to stopped thread!!!", cmd.toString() ); Log.w( TAG, "handle(%s): NOT adding to stopped thread!!!", cmd );
DbgUtils.printStack( TAG ); // DbgUtils.printStack( TAG );
} else { } else {
m_queue.add( new QueueElem( cmd, true, args ) ); m_queue.add( new QueueElem( cmd, true, args ) );
} }
@ -804,20 +822,27 @@ public class JNIThread extends Thread {
} }
} }
public static JNIThread getRetained( long rowid ) public static JNIThread getRetained( Context context, long rowid )
{ {
return getRetained( rowid, false ); return getRetained( context, rowid, false );
} }
public static JNIThread getRetained( long rowid, boolean makeNew ) public static JNIThread getRetained( Context context, long rowid, boolean makeNew )
{ {
JNIThread result = null; JNIThread result = null;
synchronized( s_instances ) { synchronized( s_instances ) {
result = s_instances.get( rowid ); result = s_instances.get( rowid );
if ( null == result && makeNew ) { if ( null == result && makeNew ) {
result = new JNIThread( new GameLock( rowid, true ).lock() ); DbgUtils.assertOnUIThread(); // can't use GameLock.lock()
Assert.assertNotNull( result ); if ( true /*test done*/ || (0 != Utils.nextRandomInt() % 3) ) {
s_instances.put( rowid, result ); GameLock lock = GameLock.getFor( rowid ).tryLock();
if ( lock != null ) {
result = new JNIThread( lock );
s_instances.put( rowid, result );
} else {
DbgUtils.toastNoLock( TAG, context, "getRetained(%d)", rowid );
}
}
} }
if ( null != result ) { if ( null != result ) {
result.retain_sync(); result.retain_sync();

View file

@ -22,8 +22,7 @@ package org.eehouse.android.xw4.jni;
import android.content.Context; import android.content.Context;
import junit.framework.Assert; import org.eehouse.android.xw4.Assert;
import org.eehouse.android.xw4.DBUtils; import org.eehouse.android.xw4.DBUtils;
import org.eehouse.android.xw4.XWApp; import org.eehouse.android.xw4.XWApp;
import org.eehouse.android.xw4.Log; import org.eehouse.android.xw4.Log;

View file

@ -25,8 +25,7 @@ import android.text.TextUtils;
import java.io.Serializable; import java.io.Serializable;
import junit.framework.Assert; import org.eehouse.android.xw4.Assert;
import org.eehouse.android.xw4.BuildConfig; import org.eehouse.android.xw4.BuildConfig;
import org.eehouse.android.xw4.R; import org.eehouse.android.xw4.R;
import org.eehouse.android.xw4.Utils; import org.eehouse.android.xw4.Utils;

View file

@ -67,7 +67,10 @@ public interface UtilCtxt {
void notifyMove( String query ); void notifyMove( String query );
void notifyTrade( String[] tiles ); void notifyTrade( String[] tiles );
// These oughtto be an enum but then I'd have to cons one up in C. // These can't be an ENUM! The set is open-ended, with arbitrary values
// added to ERR_RELAY_BASE, so no way to create an enum from an int in the
// jni world. int has to be passed into userError(). Trust me: I
// tried. :-)
static final int ERR_NONE = 0; static final int ERR_NONE = 0;
static final int ERR_TILES_NOT_IN_LINE = 1; static final int ERR_TILES_NOT_IN_LINE = 1;
static final int ERR_NO_EMPTIES_IN_TURN = 2; static final int ERR_NO_EMPTIES_IN_TURN = 2;
@ -82,11 +85,12 @@ public interface UtilCtxt {
static final int ERR_REG_SERVER_SANS_REMOTE = 11; static final int ERR_REG_SERVER_SANS_REMOTE = 11;
static final int STR_NEED_BT_HOST_ADDR = 12; static final int STR_NEED_BT_HOST_ADDR = 12;
static final int ERR_NO_EMPTY_TRADE = 13; static final int ERR_NO_EMPTY_TRADE = 13;
static final int ERR_CANT_UNDO_TILEASSIGN = 14; static final int ERR_TOO_MANY_TRADE = 14;
static final int ERR_CANT_HINT_WHILE_DISABLED = 15; static final int ERR_CANT_UNDO_TILEASSIGN = 15;
static final int ERR_NO_HINT_FOUND = 16; static final int ERR_CANT_HINT_WHILE_DISABLED = 16;
static final int ERR_NO_HINT_FOUND = 17;
static final int ERR_RELAY_BASE = 17; static final int ERR_RELAY_BASE = 18;
void userError( int id ); void userError( int id );
void informMove( int turn, String expl, String words ); void informMove( int turn, String expl, String words );

View file

@ -22,7 +22,6 @@ package org.eehouse.android.xw4.jni;
import android.content.Context; import android.content.Context;
import junit.framework.Assert;
import org.eehouse.android.xw4.Log; import org.eehouse.android.xw4.Log;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnTypeSet; import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnTypeSet;

View file

@ -24,8 +24,7 @@ import android.graphics.Rect;
import java.util.Arrays; import java.util.Arrays;
import junit.framework.Assert; import org.eehouse.android.xw4.Assert;
import org.eehouse.android.xw4.Log; import org.eehouse.android.xw4.Log;
import org.eehouse.android.xw4.NetLaunchInfo; import org.eehouse.android.xw4.NetLaunchInfo;
import org.eehouse.android.xw4.Utils; import org.eehouse.android.xw4.Utils;
@ -59,6 +58,8 @@ public class XwJNI {
return this; return this;
} }
public long getRowid() { return m_rowid; }
// Force (via an assert in finalize() below) that this is called. It's // Force (via an assert in finalize() below) that this is called. It's
// better if jni stuff isn't being done on the finalizer thread // better if jni stuff isn't being done on the finalizer thread
public synchronized void release() public synchronized void release()
@ -365,6 +366,7 @@ public class XwJNI {
public static native void server_handleUndo( GamePtr gamePtr ); public static native void server_handleUndo( GamePtr gamePtr );
public static native boolean server_do( GamePtr gamePtr ); public static native boolean server_do( GamePtr gamePtr );
public static native void server_tilesPicked( GamePtr gamePtr, int player, int[] tiles ); public static native void server_tilesPicked( GamePtr gamePtr, int player, int[] tiles );
public static native int server_countTilesInPool( GamePtr gamePtr );
public static native String server_formatDictCounts( GamePtr gamePtr, int nCols ); public static native String server_formatDictCounts( GamePtr gamePtr, int nCols );
public static native boolean server_getGameIsOver( GamePtr gamePtr ); public static native boolean server_getGameIsOver( GamePtr gamePtr );

View file

@ -42,8 +42,8 @@ import android.widget.Spinner;
import android.widget.SpinnerAdapter; import android.widget.SpinnerAdapter;
import android.widget.TextView; import android.widget.TextView;
import junit.framework.Assert;
import org.eehouse.android.xw4.Assert;
import org.eehouse.android.xw4.DBUtils; import org.eehouse.android.xw4.DBUtils;
import org.eehouse.android.xw4.DbgUtils; import org.eehouse.android.xw4.DbgUtils;
import org.eehouse.android.xw4.Log; import org.eehouse.android.xw4.Log;

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 B

View file

@ -5,8 +5,8 @@
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
> >
<Button android:id="@+id/button_add" <Button android:id="@+id/button_scan"
android:text="@string/bt_pick_addall_button" android:text="@string/button_scan"
style="@style/evenly_spaced_horizontal" style="@style/evenly_spaced_horizontal"
/> />

View file

@ -25,6 +25,9 @@
<item android:id="@+id/games_game_copy" <item android:id="@+id/games_game_copy"
android:title="@string/list_item_copy" android:title="@string/list_item_copy"
/> />
<item android:id="@+id/games_game_netstats"
android:title="@string/list_item_netstats"
/>
<item android:id="@+id/games_game_select" <item android:id="@+id/games_game_select"
android:title="@string/list_item_select" android:title="@string/list_item_select"
/> />

View file

@ -92,8 +92,8 @@
<string name="key_udp_interval">key_udp_interval</string> <string name="key_udp_interval">key_udp_interval</string>
<string name="key_notagain_sync">key_notagain_sync</string> <string name="key_notagain_sync">key_notagain_sync</string>
<!-- <string name="key_notagain_sms_ready">key_notagain_sms_ready</string> -->
<string name="key_notagain_newselect">key_notagain_newselect</string> <string name="key_notagain_newselect">key_notagain_newselect</string>
<string name="key_notagain_emptybtscan">key_notagain_emptybtscan</string>
<string name="key_notagain_backclears">key_notagain_backclears</string> <string name="key_notagain_backclears">key_notagain_backclears</string>
<string name="key_notagain_chat">key_notagain_chat</string> <string name="key_notagain_chat">key_notagain_chat</string>
<string name="key_notagain_relay">key_notagain_relay</string> <string name="key_notagain_relay">key_notagain_relay</string>
@ -136,7 +136,6 @@
<string name="key_invite_multi">key_invite_multi</string> <string name="key_invite_multi">key_invite_multi</string>
<string name="key_na_rematch_two_only">key_notagain_rematch_two_only</string> <string name="key_na_rematch_two_only">key_notagain_rematch_two_only</string>
<string name="key_notagain_dfltname">key_notagain_dfltname</string> <string name="key_notagain_dfltname">key_notagain_dfltname</string>
<string name="key_na_comms_bt">key_na_comms_bt</string> <string name="key_na_comms_bt">key_na_comms_bt</string>
<string name="key_na_comms_p2p">key_na_comms_p2p</string> <string name="key_na_comms_p2p">key_na_comms_p2p</string>
<string name="key_na_comms_sms">key_na_comms_sms</string> <string name="key_na_comms_sms">key_na_comms_sms</string>

View file

@ -148,6 +148,9 @@
<!-- Title of dialog for renaming game (triggered by selecting <!-- Title of dialog for renaming game (triggered by selecting
list_item_rename) --> list_item_rename) -->
<!-- long-tap game list menuitem for showing info about game's connectivity -->
<string name="list_item_netstats">Connections…</string>
<!-- If you try to copy a networked game you get this error <!-- If you try to copy a networked game you get this error
message. --> message. -->
<string name="no_copy_network">Games that have already connected <string name="no_copy_network">Games that have already connected
@ -1319,6 +1322,14 @@
<string name="str_too_few_tiles_left_to_trade">Too few tiles left <string name="str_too_few_tiles_left_to_trade">Too few tiles left
to exchange.</string> to exchange.</string>
<!-- In Spanish you can trade with fewer than 7 tiles in the
pool, so you can wind up selecting more than are
available. You get this error message in that case. -->
<plurals name="too_many_trade_fmt">
<item quantity="one">Too many tiles selected. Please select only one.</item>
<item quantity="other">Too many tiles selected. Please select no more than %1$d.</item>
</plurals>
<!-- Displayed if you try to use the undo menuitem or button and <!-- Displayed if you try to use the undo menuitem or button and
there are no tiles on the board (no move has yet been made.) there are no tiles on the board (no move has yet been made.)
[If I'm being clever and disabling those features in this [If I'm being clever and disabling those features in this
@ -1813,15 +1824,18 @@
<!-- <string name="newgame_enable_bt">Turn Bluetooth on</string> --> <!-- <string name="newgame_enable_bt">Turn Bluetooth on</string> -->
<!-- In the Bluetooth invite device dialog --> <!-- In invitation dialogs, button to remove checked items -->
<string name="bt_pick_addall_button">Add all Paired</string> <string name="bt_pick_clear_button">Remove</string>
<!-- -->
<string name="bt_pick_clear_button">Remove checked</string> <!-- In the Bluetooth invite device dialog, to launch Andorid BT
<!-- --> settings pair new device -->
<string name="bt_pair_settings">Pair More</string> <string name="bt_pair_settings">Pair More</string>
<!-- In the Bluetooth invite device dialog, kick off a new scan of
nearby devices -->
<string name="button_scan">Rescan</string>
<!-- --> <!-- -->
<string name="invite_progress_title">Connecting...</string> <string name="invite_progress_title">Connecting</string>
<string name="invite_progress_fmt">Sending invitation to CrossWords on %1$s</string> <string name="invite_progress_fmt">Sending invitation to CrossWords on %1$s</string>
<!-- --> <!-- -->
<string name="summary_wait_host">Waiting for connection[s]</string> <string name="summary_wait_host">Waiting for connection[s]</string>
@ -1837,7 +1851,6 @@
<string name="new_bt_body_fmt">A player on the device %1$s wants to start a game</string> <string name="new_bt_body_fmt">A player on the device %1$s wants to start a game</string>
<string name="new_relay_body">Tap to open the new game</string> <string name="new_relay_body">Tap to open the new game</string>
<!-- --> <!-- -->
<string name="bt_bad_proto_fmt">The version of CrossWords on <string name="bt_bad_proto_fmt">The version of CrossWords on
\"%1$s\" is incompatible with this one for play using \"%1$s\" is incompatible with this one for play using
@ -1850,13 +1863,20 @@
continue.</string> continue.</string>
<!-- --> <!-- -->
<plurals name="invite_bt_desc_fmt"> <plurals name="invite_bt_desc_fmt_2">
<item quantity="one">Please check the device <item quantity="one">Please check the device you want to include
you want to include in this game.\n\nUse the \"%2$s\" in this game.</item>
button if you don\'t see the device you expect.</item> <item quantity="other">Please check up to %1$d device[s] you
<item quantity="other">Please check up to %1$d device[s] want to include in this game.</item>
you want to include in this game.\n\nUse the \"%2$s\" </plurals>
button if you don\'t see a device you expect.</item>
<!-- Appended to above -->
<string name="invite_bt_desc_postscript">\n\n(The list is of devices you\'ve
paired and on which CrossWords has been detected.)</string>
<plurals name="bt_scan_progress_fmt">
<item quantity="one">Scanning for CrossWords</item>
<item quantity="other">Scanning for CrossWords on %1$d paired devices.</item>
</plurals> </plurals>
<!-- --> <!-- -->
<string name="bt_resend_fmt">Bluetooth send to %1$s failed; retry <string name="bt_resend_fmt">Bluetooth send to %1$s failed; retry
@ -2026,6 +2046,10 @@
<string name="title_enable_p2p">Enable WiFi Direct</string> <string name="title_enable_p2p">Enable WiFi Direct</string>
<string name="summary_enable_p2p">Experimental, uses lots of battery</string> <string name="summary_enable_p2p">Experimental, uses lots of battery</string>
<string name="bkng_notify_text">Accepting Bluetooth messages…</string>
<string name="bkng_stop_text">Stop</string>
<string name="bkng_settings_text">Settings</string>
<!-- --> <!-- -->
<string name="confirm_sms_title">Confirm your SMS plan</string> <string name="confirm_sms_title">Confirm your SMS plan</string>
<!-- --> <!-- -->
@ -2393,6 +2417,9 @@
within Bluetooth range and that CrossWords is installed on within Bluetooth range and that CrossWords is installed on
it.</string> it.</string>
<!-- How long ago did we last see a BT device via scan -->
<string name="bt_scan_age_fmt">Last scan response: %1$s</string>
<!-- label within default wordlists in app preferences --> <!-- label within default wordlists in app preferences -->
<string name="default_language">Default language</string> <string name="default_language">Default language</string>
@ -2447,7 +2474,7 @@
<string name="waiting_invite_title">Waiting for response</string> <string name="waiting_invite_title">Waiting for response</string>
<string name="waiting_rematch_title">Rematch in progress</string> <string name="waiting_rematch_title">Rematch in progress</string>
<!-- Button for alert with title above --> <!-- Button for alert with title above -->
<string name="button_wait">Wait</string> <string name="button_close">Close</string>
<string name="button_reinvite">Re-invite</string> <string name="button_reinvite">Re-invite</string>
<string name="invite_stays">(This dialog will stay up until all <string name="invite_stays">(This dialog will stay up until all
@ -2641,6 +2668,12 @@
player name \"%1$s\". Would you like to personalize with your own player name \"%1$s\". Would you like to personalize with your own
name before you create this game?</string> name before you create this game?</string>
<string name="btservice_expl">This notification is present whenever
CrossWords is running in the background to accept Bluetooth
messages. It usually runs for about 15 minutes after CrossWords
starts or a Bluetooth message is received.
</string>
<string name="no_invites">This game has sent no invitations</string> <string name="no_invites">This game has sent no invitations</string>
<string name="disable_dualpane">Disable side-by-side</string> <string name="disable_dualpane">Disable side-by-side</string>
@ -2715,4 +2748,14 @@
<string name="perms_rationale_title">Android Permissions</string> <string name="perms_rationale_title">Android Permissions</string>
<string name="toast_no_permission">Permission not granted</string> <string name="toast_no_permission">Permission not granted</string>
<!-- Explanation in settings for always-on BT notification -->
<string name="foreground_channel_expl">Accepting Bluetooth in background</string>
<!-- Explanation in settings for traditional move-arrived notification -->
<string name="gameevent_channel_expl">In-game events</string>
<string name="not_again_emptybtscan">If a scan doesn\'t find the device you expect:\n
• First, just try again\n
• Make sure Bluetooth is on on the other device\n
• Launch CrossWords on the other device\n
</string>
</resources> </resources>

View file

@ -29,7 +29,6 @@ import com.google.android.gcm.GCMRegistrar;
import org.json.JSONArray; import org.json.JSONArray;
import junit.framework.Assert;
public class GCMIntentService extends GCMBaseIntentService { public class GCMIntentService extends GCMBaseIntentService {
private static final String TAG = GCMIntentService.class.getSimpleName(); private static final String TAG = GCMIntentService.class.getSimpleName();

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

View file

@ -2,9 +2,9 @@
buildscript { buildscript {
repositories { repositories {
google()
jcenter() jcenter()
maven { url 'https://maven.fabric.io/public' } maven { url 'https://maven.fabric.io/public' }
google()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.1.2' classpath 'com.android.tools.build:gradle:3.1.2'
@ -17,6 +17,7 @@ buildscript {
allprojects { allprojects {
repositories { repositories {
google()
jcenter() jcenter()
} }
} }
@ -29,7 +30,7 @@ subprojects {
afterEvaluate {project -> afterEvaluate {project ->
if (project.hasProperty("android")) { if (project.hasProperty("android")) {
android { android {
compileSdkVersion 23 compileSdkVersion 26
} }
} }
} }

View file

@ -103,6 +103,15 @@ and_util_getMD5SumForDict( JNIUtilCtxt* jniutil, const XP_UCHAR* name,
jstring result = jstring result =
(*env)->CallObjectMethod( env, jniutil->jjniutil, mid, jname, jbytes ); (*env)->CallObjectMethod( env, jniutil->jjniutil, mid, jname, jbytes );
deleteLocalRefs( env, jname, jbytes, DELETE_NO_REF ); deleteLocalRefs( env, jname, jbytes, DELETE_NO_REF );
#ifdef DEBUG
if ( !!result ) {
const char* chars = (*env)->GetStringUTFChars( env, result, NULL );
XP_LOGF( "%s(%s, len=%d) => %s", __func__, name, len, chars );
(*env)->ReleaseStringUTFChars( env, result, chars );
}
#endif
return result; return result;
} }

View file

@ -702,10 +702,10 @@ and_dutil_deviceRegistered( XW_DUtilCtxt* duc, DevIDType typ,
const XP_UCHAR* idRelay ) const XP_UCHAR* idRelay )
{ {
DUTIL_CBK_HEADER( "deviceRegistered", DUTIL_CBK_HEADER( "deviceRegistered",
"(L" PKG_PATH("jni/UtilCtxt$DevIDType") ";Ljava/lang/String;)V" ); "(L" PKG_PATH("jni/DUtilCtxt$DevIDType") ";Ljava/lang/String;)V" );
jstring jstr = (*env)->NewStringUTF( env, idRelay ); jstring jstr = (*env)->NewStringUTF( env, idRelay );
jobject jtyp = intToJEnum( env, typ, jobject jtyp = intToJEnum( env, typ,
PKG_PATH("jni/UtilCtxt$DevIDType") ); PKG_PATH("jni/DUtilCtxt$DevIDType") );
(*env)->CallVoidMethod( env, dutil->jdutil, mid, jtyp, jstr ); (*env)->CallVoidMethod( env, dutil->jdutil, mid, jtyp, jstr );
deleteLocalRefs( env, jstr, jtyp, DELETE_NO_REF ); deleteLocalRefs( env, jstr, jtyp, DELETE_NO_REF );
DUTIL_CBK_TAIL(); DUTIL_CBK_TAIL();

Some files were not shown because too many files have changed in this diff Show more