diff --git a/app/build.gradle b/app/build.gradle index 94c7b8a..c258254 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -33,8 +33,4 @@ dependencies { compile 'com.android.support:support-v4:23.3.0' compile 'com.android.support:recyclerview-v7:23.3.0' compile 'com.android.support:design:23.3.0' - compile 'com.embarkmobile:zxing-android-minimal:2.0@aar' - compile 'com.embarkmobile:zxing-android-legacy:2.0.0@aar' - compile 'com.embarkmobile:zxing-android-integration:2.0.0@aar' - compile 'com.google.zxing:core:3.0.1' } diff --git a/app/libs/android-core-3.2.2-SNAPSHOT.jar b/app/libs/android-core-3.2.2-SNAPSHOT.jar new file mode 100644 index 0000000..503f817 Binary files /dev/null and b/app/libs/android-core-3.2.2-SNAPSHOT.jar differ diff --git a/app/libs/core-3.2.2-SNAPSHOT.jar b/app/libs/core-3.2.2-SNAPSHOT.jar new file mode 100644 index 0000000..4ed7e27 Binary files /dev/null and b/app/libs/core-3.2.2-SNAPSHOT.jar differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5113f17..2580491 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/google/zxing/client/android/AmbientLightManager.java b/app/src/main/java/com/google/zxing/client/android/AmbientLightManager.java new file mode 100644 index 0000000..2074265 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/AmbientLightManager.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2012 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android; + +import android.content.Context; +import android.content.SharedPreferences; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.preference.PreferenceManager; +import com.google.zxing.client.android.camera.CameraManager; +import com.google.zxing.client.android.camera.FrontLightMode; + +/** + * Detects ambient light and switches on the front light when very dark, and off again when sufficiently light. + * + * @author Sean Owen + * @author Nikolaus Huber + */ +final class AmbientLightManager implements SensorEventListener { + + private static final float TOO_DARK_LUX = 45.0f; + private static final float BRIGHT_ENOUGH_LUX = 450.0f; + + private final Context context; + private CameraManager cameraManager; + private Sensor lightSensor; + + AmbientLightManager(Context context) { + this.context = context; + } + + void start(CameraManager cameraManager) { + this.cameraManager = cameraManager; + SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); + if (FrontLightMode.readPref(sharedPrefs) == FrontLightMode.AUTO) { + SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + lightSensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT); + if (lightSensor != null) { + sensorManager.registerListener(this, lightSensor, SensorManager.SENSOR_DELAY_NORMAL); + } + } + } + + void stop() { + if (lightSensor != null) { + SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + sensorManager.unregisterListener(this); + cameraManager = null; + lightSensor = null; + } + } + + @Override + public void onSensorChanged(SensorEvent sensorEvent) { + float ambientLightLux = sensorEvent.values[0]; + if (cameraManager != null) { + if (ambientLightLux <= TOO_DARK_LUX) { + cameraManager.setTorch(true); + } else if (ambientLightLux >= BRIGHT_ENOUGH_LUX) { + cameraManager.setTorch(false); + } + } + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + // do nothing + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/BeepManager.java b/app/src/main/java/com/google/zxing/client/android/BeepManager.java new file mode 100644 index 0000000..f4f2e58 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/BeepManager.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android; + +import android.app.Activity; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.AssetFileDescriptor; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.os.Vibrator; +import android.preference.PreferenceManager; +import android.util.Log; + +import net.foucry.pilldroid.R; + +import java.io.Closeable; +import java.io.IOException; + +/** + * Manages beeps and vibrations for {@link CaptureActivity}. + */ +final class BeepManager implements MediaPlayer.OnErrorListener, Closeable { + + private static final String TAG = BeepManager.class.getSimpleName(); + + private static final float BEEP_VOLUME = 0.10f; + private static final long VIBRATE_DURATION = 200L; + + private final Activity activity; + private MediaPlayer mediaPlayer; + private boolean playBeep; + private boolean vibrate; + + BeepManager(Activity activity) { + this.activity = activity; + this.mediaPlayer = null; + updatePrefs(); + } + + synchronized void updatePrefs() { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity); + playBeep = shouldBeep(prefs, activity); + vibrate = prefs.getBoolean(PreferencesActivity.KEY_VIBRATE, false); + if (playBeep && mediaPlayer == null) { + // The volume on STREAM_SYSTEM is not adjustable, and users found it too loud, + // so we now play on the music stream. + activity.setVolumeControlStream(AudioManager.STREAM_MUSIC); + mediaPlayer = buildMediaPlayer(activity); + } + } + + synchronized void playBeepSoundAndVibrate() { + if (playBeep && mediaPlayer != null) { + mediaPlayer.start(); + } + if (vibrate) { + Vibrator vibrator = (Vibrator) activity.getSystemService(Context.VIBRATOR_SERVICE); + vibrator.vibrate(VIBRATE_DURATION); + } + } + + private static boolean shouldBeep(SharedPreferences prefs, Context activity) { + boolean shouldPlayBeep = prefs.getBoolean(PreferencesActivity.KEY_PLAY_BEEP, true); + if (shouldPlayBeep) { + // See if sound settings overrides this + AudioManager audioService = (AudioManager) activity.getSystemService(Context.AUDIO_SERVICE); + if (audioService.getRingerMode() != AudioManager.RINGER_MODE_NORMAL) { + shouldPlayBeep = false; + } + } + return shouldPlayBeep; + } + + private MediaPlayer buildMediaPlayer(Context activity) { + MediaPlayer mediaPlayer = new MediaPlayer(); + try { + AssetFileDescriptor file = activity.getResources().openRawResourceFd(R.raw.beep); + try { + mediaPlayer.setDataSource(file.getFileDescriptor(), file.getStartOffset(), file.getLength()); + } finally { + file.close(); + } + mediaPlayer.setOnErrorListener(this); + mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + mediaPlayer.setLooping(false); + mediaPlayer.setVolume(BEEP_VOLUME, BEEP_VOLUME); + mediaPlayer.prepare(); + return mediaPlayer; + } catch (IOException ioe) { + Log.w(TAG, ioe); + mediaPlayer.release(); + return null; + } + } + + @Override + public synchronized boolean onError(MediaPlayer mp, int what, int extra) { + if (what == MediaPlayer.MEDIA_ERROR_SERVER_DIED) { + // we are finished, so put up an appropriate error toast if required and finish + activity.finish(); + } else { + // possibly media player error, so release and recreate + close(); + updatePrefs(); + } + return true; + } + + @Override + public synchronized void close() { + if (mediaPlayer != null) { + mediaPlayer.release(); + mediaPlayer = null; + } + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/CaptureActivity.java b/app/src/main/java/com/google/zxing/client/android/CaptureActivity.java new file mode 100644 index 0000000..2263eb2 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/CaptureActivity.java @@ -0,0 +1,769 @@ +/* + * Copyright (C) 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.DecodeHintType; +import com.google.zxing.Result; +import com.google.zxing.ResultMetadataType; +import com.google.zxing.ResultPoint; +import com.google.zxing.client.android.camera.CameraManager; +import com.google.zxing.client.android.clipboard.ClipboardInterface; +import com.google.zxing.client.android.history.HistoryActivity; +import com.google.zxing.client.android.history.HistoryItem; +import com.google.zxing.client.android.history.HistoryManager; +import com.google.zxing.client.android.result.ResultButtonListener; +import com.google.zxing.client.android.result.ResultHandler; +import com.google.zxing.client.android.result.ResultHandlerFactory; +import com.google.zxing.client.android.result.supplement.SupplementalInfoRetriever; +import com.google.zxing.client.android.share.ShareActivity; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.preference.PreferenceManager; +import android.util.Log; +import android.util.TypedValue; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import net.foucry.pilldroid.R; + +import java.io.IOException; +import java.text.DateFormat; +import java.util.Collection; +import java.util.Date; +import java.util.EnumSet; +import java.util.Map; + +/** + * This activity opens the camera and does the actual scanning on a background thread. It draws a + * viewfinder to help the user place the barcode correctly, shows feedback as the image processing + * is happening, and then overlays the results when a scan is successful. + * + * @author dswitkin@google.com (Daniel Switkin) + * @author Sean Owen + */ +public final class CaptureActivity extends Activity implements SurfaceHolder.Callback { + + private static final String TAG = CaptureActivity.class.getSimpleName(); + + private static final long DEFAULT_INTENT_RESULT_DURATION_MS = 1500L; + private static final long BULK_MODE_SCAN_DELAY_MS = 1000L; + + private static final String[] ZXING_URLS = { "http://zxing.appspot.com/scan", "zxing://scan/" }; + + public static final int HISTORY_REQUEST_CODE = 0x0000bacc; + + private static final Collection DISPLAYABLE_METADATA_TYPES = + EnumSet.of(ResultMetadataType.ISSUE_NUMBER, + ResultMetadataType.SUGGESTED_PRICE, + ResultMetadataType.ERROR_CORRECTION_LEVEL, + ResultMetadataType.POSSIBLE_COUNTRY); + + private CameraManager cameraManager; + private CaptureActivityHandler handler; + private Result savedResultToShow; + private ViewfinderView viewfinderView; + private TextView statusView; + private View resultView; + private Result lastResult; + private boolean hasSurface; + private boolean copyToClipboard; + private IntentSource source; + private String sourceUrl; + private ScanFromWebPageManager scanFromWebPageManager; + private Collection decodeFormats; + private Map decodeHints; + private String characterSet; + private HistoryManager historyManager; + private InactivityTimer inactivityTimer; + private BeepManager beepManager; + private AmbientLightManager ambientLightManager; + + ViewfinderView getViewfinderView() { + return viewfinderView; + } + + public Handler getHandler() { + return handler; + } + + CameraManager getCameraManager() { + return cameraManager; + } + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + Window window = getWindow(); + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + setContentView(R.layout.capture); + + hasSurface = false; + inactivityTimer = new InactivityTimer(this); + beepManager = new BeepManager(this); + ambientLightManager = new AmbientLightManager(this); + + PreferenceManager.setDefaultValues(this, R.xml.preferences, false); + } + + @Override + protected void onResume() { + super.onResume(); + + // historyManager must be initialized here to update the history preference + historyManager = new HistoryManager(this); + historyManager.trimHistory(); + + // CameraManager must be initialized here, not in onCreate(). This is necessary because we don't + // want to open the camera driver and measure the screen size if we're going to show the help on + // first launch. That led to bugs where the scanning rectangle was the wrong size and partially + // off screen. + cameraManager = new CameraManager(getApplication()); + + viewfinderView = (ViewfinderView) findViewById(R.id.viewfinder_view); + viewfinderView.setCameraManager(cameraManager); + + resultView = findViewById(R.id.result_view); + statusView = (TextView) findViewById(R.id.status_view); + + handler = null; + lastResult = null; + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + + if (prefs.getBoolean(PreferencesActivity.KEY_DISABLE_AUTO_ORIENTATION, true)) { + setRequestedOrientation(getCurrentOrientation()); + } else { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE); + } + + resetStatusView(); + + + beepManager.updatePrefs(); + ambientLightManager.start(cameraManager); + + inactivityTimer.onResume(); + + Intent intent = getIntent(); + + copyToClipboard = prefs.getBoolean(PreferencesActivity.KEY_COPY_TO_CLIPBOARD, true) + && (intent == null || intent.getBooleanExtra(Intents.Scan.SAVE_HISTORY, true)); + + source = IntentSource.NONE; + sourceUrl = null; + scanFromWebPageManager = null; + decodeFormats = null; + characterSet = null; + + if (intent != null) { + + String action = intent.getAction(); + String dataString = intent.getDataString(); + + if (Intents.Scan.ACTION.equals(action)) { + + // Scan the formats the intent requested, and return the result to the calling activity. + source = IntentSource.NATIVE_APP_INTENT; + decodeFormats = DecodeFormatManager.parseDecodeFormats(intent); + decodeHints = DecodeHintManager.parseDecodeHints(intent); + + if (intent.hasExtra(Intents.Scan.WIDTH) && intent.hasExtra(Intents.Scan.HEIGHT)) { + int width = intent.getIntExtra(Intents.Scan.WIDTH, 0); + int height = intent.getIntExtra(Intents.Scan.HEIGHT, 0); + if (width > 0 && height > 0) { + cameraManager.setManualFramingRect(width, height); + } + } + + if (intent.hasExtra(Intents.Scan.CAMERA_ID)) { + int cameraId = intent.getIntExtra(Intents.Scan.CAMERA_ID, -1); + if (cameraId >= 0) { + cameraManager.setManualCameraId(cameraId); + } + } + + String customPromptMessage = intent.getStringExtra(Intents.Scan.PROMPT_MESSAGE); + if (customPromptMessage != null) { + statusView.setText(customPromptMessage); + } + + } else if (dataString != null && + dataString.contains("http://www.google") && + dataString.contains("/m/products/scan")) { + + // Scan only products and send the result to mobile Product Search. + source = IntentSource.PRODUCT_SEARCH_LINK; + sourceUrl = dataString; + decodeFormats = DecodeFormatManager.PRODUCT_FORMATS; + + } else if (isZXingURL(dataString)) { + + // Scan formats requested in query string (all formats if none specified). + // If a return URL is specified, send the results there. Otherwise, handle it ourselves. + source = IntentSource.ZXING_LINK; + sourceUrl = dataString; + Uri inputUri = Uri.parse(dataString); + scanFromWebPageManager = new ScanFromWebPageManager(inputUri); + decodeFormats = DecodeFormatManager.parseDecodeFormats(inputUri); + // Allow a sub-set of the hints to be specified by the caller. + decodeHints = DecodeHintManager.parseDecodeHints(inputUri); + + } + + characterSet = intent.getStringExtra(Intents.Scan.CHARACTER_SET); + + } + + SurfaceView surfaceView = (SurfaceView) findViewById(R.id.preview_view); + SurfaceHolder surfaceHolder = surfaceView.getHolder(); + if (hasSurface) { + // The activity was paused but not stopped, so the surface still exists. Therefore + // surfaceCreated() won't be called, so init the camera here. + initCamera(surfaceHolder); + } else { + // Install the callback and wait for surfaceCreated() to init the camera. + surfaceHolder.addCallback(this); + } + } + + private int getCurrentOrientation() { + int rotation = getWindowManager().getDefaultDisplay().getRotation(); + if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { + switch (rotation) { + case Surface.ROTATION_0: + case Surface.ROTATION_90: + return ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; + default: + return ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE; + } + } else { + switch (rotation) { + case Surface.ROTATION_0: + case Surface.ROTATION_270: + return ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; + default: + return ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT; + } + } + } + + private static boolean isZXingURL(String dataString) { + if (dataString == null) { + return false; + } + for (String url : ZXING_URLS) { + if (dataString.startsWith(url)) { + return true; + } + } + return false; + } + + @Override + protected void onPause() { + if (handler != null) { + handler.quitSynchronously(); + handler = null; + } + inactivityTimer.onPause(); + ambientLightManager.stop(); + beepManager.close(); + cameraManager.closeDriver(); + //historyManager = null; // Keep for onActivityResult + if (!hasSurface) { + SurfaceView surfaceView = (SurfaceView) findViewById(R.id.preview_view); + SurfaceHolder surfaceHolder = surfaceView.getHolder(); + surfaceHolder.removeCallback(this); + } + super.onPause(); + } + + @Override + protected void onDestroy() { + inactivityTimer.shutdown(); + super.onDestroy(); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_BACK: + if (source == IntentSource.NATIVE_APP_INTENT) { + setResult(RESULT_CANCELED); + finish(); + return true; + } + if ((source == IntentSource.NONE || source == IntentSource.ZXING_LINK) && lastResult != null) { + restartPreviewAfterDelay(0L); + return true; + } + break; + case KeyEvent.KEYCODE_FOCUS: + case KeyEvent.KEYCODE_CAMERA: + // Handle these events so they don't launch the Camera app + return true; + // Use volume up/down to turn on light + case KeyEvent.KEYCODE_VOLUME_DOWN: + cameraManager.setTorch(false); + return true; + case KeyEvent.KEYCODE_VOLUME_UP: + cameraManager.setTorch(true); + return true; + } + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater menuInflater = getMenuInflater(); + menuInflater.inflate(R.menu.capture, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + switch (item.getItemId()) { + case R.id.menu_share: + intent.setClassName(this, ShareActivity.class.getName()); + startActivity(intent); + break; + case R.id.menu_history: + intent.setClassName(this, HistoryActivity.class.getName()); + startActivityForResult(intent, HISTORY_REQUEST_CODE); + break; + case R.id.menu_settings: + intent.setClassName(this, PreferencesActivity.class.getName()); + startActivity(intent); + break; + case R.id.menu_help: + intent.setClassName(this, HelpActivity.class.getName()); + startActivity(intent); + break; + default: + return super.onOptionsItemSelected(item); + } + return true; + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent intent) { + if (resultCode == RESULT_OK && requestCode == HISTORY_REQUEST_CODE && historyManager != null) { + int itemNumber = intent.getIntExtra(Intents.History.ITEM_NUMBER, -1); + if (itemNumber >= 0) { + HistoryItem historyItem = historyManager.buildHistoryItem(itemNumber); + decodeOrStoreSavedBitmap(null, historyItem.getResult()); + } + } + } + + private void decodeOrStoreSavedBitmap(Bitmap bitmap, Result result) { + // Bitmap isn't used yet -- will be used soon + if (handler == null) { + savedResultToShow = result; + } else { + if (result != null) { + savedResultToShow = result; + } + if (savedResultToShow != null) { + Message message = Message.obtain(handler, R.id.decode_succeeded, savedResultToShow); + handler.sendMessage(message); + } + savedResultToShow = null; + } + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + if (holder == null) { + Log.e(TAG, "*** WARNING *** surfaceCreated() gave us a null surface!"); + } + if (!hasSurface) { + hasSurface = true; + initCamera(holder); + } + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + hasSurface = false; + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + + } + + /** + * A valid barcode has been found, so give an indication of success and show the results. + * + * @param rawResult The contents of the barcode. + * @param scaleFactor amount by which thumbnail was scaled + * @param barcode A greyscale bitmap of the camera data which was decoded. + */ + public void handleDecode(Result rawResult, Bitmap barcode, float scaleFactor) { + inactivityTimer.onActivity(); + lastResult = rawResult; + ResultHandler resultHandler = ResultHandlerFactory.makeResultHandler(this, rawResult); + + boolean fromLiveScan = barcode != null; + if (fromLiveScan) { + historyManager.addHistoryItem(rawResult, resultHandler); + // Then not from history, so beep/vibrate and we have an image to draw on + beepManager.playBeepSoundAndVibrate(); + drawResultPoints(barcode, scaleFactor, rawResult); + } + + switch (source) { + case NATIVE_APP_INTENT: + case PRODUCT_SEARCH_LINK: + handleDecodeExternally(rawResult, resultHandler, barcode); + break; + case ZXING_LINK: + if (scanFromWebPageManager == null || !scanFromWebPageManager.isScanFromWebPage()) { + handleDecodeInternally(rawResult, resultHandler, barcode); + } else { + handleDecodeExternally(rawResult, resultHandler, barcode); + } + break; + case NONE: + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + if (fromLiveScan && prefs.getBoolean(PreferencesActivity.KEY_BULK_MODE, false)) { + Toast.makeText(getApplicationContext(), + getResources().getString(R.string.msg_bulk_mode_scanned) + " (" + rawResult.getText() + ')', + Toast.LENGTH_SHORT).show(); + // Wait a moment or else it will scan the same barcode continuously about 3 times + restartPreviewAfterDelay(BULK_MODE_SCAN_DELAY_MS); + } else { + handleDecodeInternally(rawResult, resultHandler, barcode); + } + break; + } + } + + /** + * Superimpose a line for 1D or dots for 2D to highlight the key features of the barcode. + * + * @param barcode A bitmap of the captured image. + * @param scaleFactor amount by which thumbnail was scaled + * @param rawResult The decoded results which contains the points to draw. + */ + private void drawResultPoints(Bitmap barcode, float scaleFactor, Result rawResult) { + ResultPoint[] points = rawResult.getResultPoints(); + if (points != null && points.length > 0) { + Canvas canvas = new Canvas(barcode); + Paint paint = new Paint(); + paint.setColor(getResources().getColor(R.color.result_points)); + if (points.length == 2) { + paint.setStrokeWidth(4.0f); + drawLine(canvas, paint, points[0], points[1], scaleFactor); + } else if (points.length == 4 && + (rawResult.getBarcodeFormat() == BarcodeFormat.UPC_A || + rawResult.getBarcodeFormat() == BarcodeFormat.EAN_13)) { + // Hacky special case -- draw two lines, for the barcode and metadata + drawLine(canvas, paint, points[0], points[1], scaleFactor); + drawLine(canvas, paint, points[2], points[3], scaleFactor); + } else { + paint.setStrokeWidth(10.0f); + for (ResultPoint point : points) { + if (point != null) { + canvas.drawPoint(scaleFactor * point.getX(), scaleFactor * point.getY(), paint); + } + } + } + } + } + + private static void drawLine(Canvas canvas, Paint paint, ResultPoint a, ResultPoint b, float scaleFactor) { + if (a != null && b != null) { + canvas.drawLine(scaleFactor * a.getX(), + scaleFactor * a.getY(), + scaleFactor * b.getX(), + scaleFactor * b.getY(), + paint); + } + } + + // Put up our own UI for how to handle the decoded contents. + private void handleDecodeInternally(Result rawResult, ResultHandler resultHandler, Bitmap barcode) { + + CharSequence displayContents = resultHandler.getDisplayContents(); + + if (copyToClipboard && !resultHandler.areContentsSecure()) { + ClipboardInterface.setText(displayContents, this); + } + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + + if (resultHandler.getDefaultButtonID() != null && prefs.getBoolean(PreferencesActivity.KEY_AUTO_OPEN_WEB, false)) { + resultHandler.handleButtonPress(resultHandler.getDefaultButtonID()); + return; + } + + statusView.setVisibility(View.GONE); + viewfinderView.setVisibility(View.GONE); + resultView.setVisibility(View.VISIBLE); + + ImageView barcodeImageView = (ImageView) findViewById(R.id.barcode_image_view); + if (barcode == null) { + barcodeImageView.setImageBitmap(BitmapFactory.decodeResource(getResources(), + R.mipmap.ic_launcher)); + } else { + barcodeImageView.setImageBitmap(barcode); + } + + TextView formatTextView = (TextView) findViewById(R.id.format_text_view); + formatTextView.setText(rawResult.getBarcodeFormat().toString()); + + TextView typeTextView = (TextView) findViewById(R.id.type_text_view); + typeTextView.setText(resultHandler.getType().toString()); + + DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT); + TextView timeTextView = (TextView) findViewById(R.id.time_text_view); + timeTextView.setText(formatter.format(new Date(rawResult.getTimestamp()))); + + + TextView metaTextView = (TextView) findViewById(R.id.meta_text_view); + View metaTextViewLabel = findViewById(R.id.meta_text_view_label); + metaTextView.setVisibility(View.GONE); + metaTextViewLabel.setVisibility(View.GONE); + Map metadata = rawResult.getResultMetadata(); + if (metadata != null) { + StringBuilder metadataText = new StringBuilder(20); + for (Map.Entry entry : metadata.entrySet()) { + if (DISPLAYABLE_METADATA_TYPES.contains(entry.getKey())) { + metadataText.append(entry.getValue()).append('\n'); + } + } + if (metadataText.length() > 0) { + metadataText.setLength(metadataText.length() - 1); + metaTextView.setText(metadataText); + metaTextView.setVisibility(View.VISIBLE); + metaTextViewLabel.setVisibility(View.VISIBLE); + } + } + + TextView contentsTextView = (TextView) findViewById(R.id.contents_text_view); + contentsTextView.setText(displayContents); + int scaledSize = Math.max(22, 32 - displayContents.length() / 4); + contentsTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, scaledSize); + + TextView supplementTextView = (TextView) findViewById(R.id.contents_supplement_text_view); + supplementTextView.setText(""); + supplementTextView.setOnClickListener(null); + if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean( + PreferencesActivity.KEY_SUPPLEMENTAL, true)) { + SupplementalInfoRetriever.maybeInvokeRetrieval(supplementTextView, + resultHandler.getResult(), + historyManager, + this); + } + + int buttonCount = resultHandler.getButtonCount(); + ViewGroup buttonView = (ViewGroup) findViewById(R.id.result_button_view); + buttonView.requestFocus(); + for (int x = 0; x < ResultHandler.MAX_BUTTON_COUNT; x++) { + TextView button = (TextView) buttonView.getChildAt(x); + if (x < buttonCount) { + button.setVisibility(View.VISIBLE); + button.setText(resultHandler.getButtonText(x)); + button.setOnClickListener(new ResultButtonListener(resultHandler, x)); + } else { + button.setVisibility(View.GONE); + } + } + + } + + // Briefly show the contents of the barcode, then handle the result outside Barcode Scanner. + private void handleDecodeExternally(Result rawResult, ResultHandler resultHandler, Bitmap barcode) { + + if (barcode != null) { + viewfinderView.drawResultBitmap(barcode); + } + + long resultDurationMS; + if (getIntent() == null) { + resultDurationMS = DEFAULT_INTENT_RESULT_DURATION_MS; + } else { + resultDurationMS = getIntent().getLongExtra(Intents.Scan.RESULT_DISPLAY_DURATION_MS, + DEFAULT_INTENT_RESULT_DURATION_MS); + } + + if (resultDurationMS > 0) { + String rawResultString = String.valueOf(rawResult); + if (rawResultString.length() > 32) { + rawResultString = rawResultString.substring(0, 32) + " ..."; + } + statusView.setText(getString(resultHandler.getDisplayTitle()) + " : " + rawResultString); + } + + if (copyToClipboard && !resultHandler.areContentsSecure()) { + CharSequence text = resultHandler.getDisplayContents(); + ClipboardInterface.setText(text, this); + } + + if (source == IntentSource.NATIVE_APP_INTENT) { + + // Hand back whatever action they requested - this can be changed to Intents.Scan.ACTION when + // the deprecated intent is retired. + Intent intent = new Intent(getIntent().getAction()); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + intent.putExtra(Intents.Scan.RESULT, rawResult.toString()); + intent.putExtra(Intents.Scan.RESULT_FORMAT, rawResult.getBarcodeFormat().toString()); + byte[] rawBytes = rawResult.getRawBytes(); + if (rawBytes != null && rawBytes.length > 0) { + intent.putExtra(Intents.Scan.RESULT_BYTES, rawBytes); + } + Map metadata = rawResult.getResultMetadata(); + if (metadata != null) { + if (metadata.containsKey(ResultMetadataType.UPC_EAN_EXTENSION)) { + intent.putExtra(Intents.Scan.RESULT_UPC_EAN_EXTENSION, + metadata.get(ResultMetadataType.UPC_EAN_EXTENSION).toString()); + } + Number orientation = (Number) metadata.get(ResultMetadataType.ORIENTATION); + if (orientation != null) { + intent.putExtra(Intents.Scan.RESULT_ORIENTATION, orientation.intValue()); + } + String ecLevel = (String) metadata.get(ResultMetadataType.ERROR_CORRECTION_LEVEL); + if (ecLevel != null) { + intent.putExtra(Intents.Scan.RESULT_ERROR_CORRECTION_LEVEL, ecLevel); + } + @SuppressWarnings("unchecked") + Iterable byteSegments = (Iterable) metadata.get(ResultMetadataType.BYTE_SEGMENTS); + if (byteSegments != null) { + int i = 0; + for (byte[] byteSegment : byteSegments) { + intent.putExtra(Intents.Scan.RESULT_BYTE_SEGMENTS_PREFIX + i, byteSegment); + i++; + } + } + } + sendReplyMessage(R.id.return_scan_result, intent, resultDurationMS); + + } else if (source == IntentSource.PRODUCT_SEARCH_LINK) { + + // Reformulate the URL which triggered us into a query, so that the request goes to the same + // TLD as the scan URL. + int end = sourceUrl.lastIndexOf("/scan"); + String replyURL = sourceUrl.substring(0, end) + "?q=" + resultHandler.getDisplayContents() + "&source=zxing"; + sendReplyMessage(R.id.launch_product_query, replyURL, resultDurationMS); + + } else if (source == IntentSource.ZXING_LINK) { + + if (scanFromWebPageManager != null && scanFromWebPageManager.isScanFromWebPage()) { + String replyURL = scanFromWebPageManager.buildReplyURL(rawResult, resultHandler); + scanFromWebPageManager = null; + sendReplyMessage(R.id.launch_product_query, replyURL, resultDurationMS); + } + + } + } + + private void sendReplyMessage(int id, Object arg, long delayMS) { + if (handler != null) { + Message message = Message.obtain(handler, id, arg); + if (delayMS > 0L) { + handler.sendMessageDelayed(message, delayMS); + } else { + handler.sendMessage(message); + } + } + } + + private void initCamera(SurfaceHolder surfaceHolder) { + if (surfaceHolder == null) { + throw new IllegalStateException("No SurfaceHolder provided"); + } + if (cameraManager.isOpen()) { + Log.w(TAG, "initCamera() while already open -- late SurfaceView callback?"); + return; + } + try { + cameraManager.openDriver(surfaceHolder); + // Creating the handler starts the preview, which can also throw a RuntimeException. + if (handler == null) { + handler = new CaptureActivityHandler(this, decodeFormats, decodeHints, characterSet, cameraManager); + } + decodeOrStoreSavedBitmap(null, null); + } catch (IOException ioe) { + Log.w(TAG, ioe); + displayFrameworkBugMessageAndExit(); + } catch (RuntimeException e) { + // Barcode Scanner has seen crashes in the wild of this variety: + // java.?lang.?RuntimeException: Fail to connect to camera service + Log.w(TAG, "Unexpected error initializing camera", e); + displayFrameworkBugMessageAndExit(); + } + } + + private void displayFrameworkBugMessageAndExit() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.app_name)); + builder.setMessage(getString(R.string.msg_camera_framework_bug)); + builder.setPositiveButton(R.string.button_ok, new FinishListener(this)); + builder.setOnCancelListener(new FinishListener(this)); + builder.show(); + } + + public void restartPreviewAfterDelay(long delayMS) { + if (handler != null) { + handler.sendEmptyMessageDelayed(R.id.restart_preview, delayMS); + } + resetStatusView(); + } + + private void resetStatusView() { + resultView.setVisibility(View.GONE); + statusView.setText(R.string.msg_default_status); + statusView.setVisibility(View.VISIBLE); + viewfinderView.setVisibility(View.VISIBLE); + lastResult = null; + } + + public void drawViewfinder() { + viewfinderView.drawViewfinder(); + } +} diff --git a/app/src/main/java/com/google/zxing/client/android/CaptureActivityHandler.java b/app/src/main/java/com/google/zxing/client/android/CaptureActivityHandler.java new file mode 100644 index 0000000..c4698e6 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/CaptureActivityHandler.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android; + +import android.content.ActivityNotFoundException; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.graphics.BitmapFactory; +import android.provider.Browser; +import com.google.zxing.BarcodeFormat; +import com.google.zxing.DecodeHintType; +import com.google.zxing.Result; +import com.google.zxing.client.android.camera.CameraManager; + +import net.foucry.pilldroid.R; + +import android.app.Activity; +import android.content.Intent; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.util.Log; + +import java.util.Collection; +import java.util.Map; + +/** + * This class handles all the messaging which comprises the state machine for capture. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class CaptureActivityHandler extends Handler { + + private static final String TAG = CaptureActivityHandler.class.getSimpleName(); + + private final CaptureActivity activity; + private final DecodeThread decodeThread; + private State state; + private final CameraManager cameraManager; + + private enum State { + PREVIEW, + SUCCESS, + DONE + } + + CaptureActivityHandler(CaptureActivity activity, + Collection decodeFormats, + Map baseHints, + String characterSet, + CameraManager cameraManager) { + this.activity = activity; + decodeThread = new DecodeThread(activity, decodeFormats, baseHints, characterSet, + new ViewfinderResultPointCallback(activity.getViewfinderView())); + decodeThread.start(); + state = State.SUCCESS; + + // Start ourselves capturing previews and decoding. + this.cameraManager = cameraManager; + cameraManager.startPreview(); + restartPreviewAndDecode(); + } + + @Override + public void handleMessage(Message message) { + switch (message.what) { + case R.id.restart_preview: + restartPreviewAndDecode(); + break; + case R.id.decode_succeeded: + state = State.SUCCESS; + Bundle bundle = message.getData(); + Bitmap barcode = null; + float scaleFactor = 1.0f; + if (bundle != null) { + byte[] compressedBitmap = bundle.getByteArray(DecodeThread.BARCODE_BITMAP); + if (compressedBitmap != null) { + barcode = BitmapFactory.decodeByteArray(compressedBitmap, 0, compressedBitmap.length, null); + // Mutable copy: + barcode = barcode.copy(Bitmap.Config.ARGB_8888, true); + } + scaleFactor = bundle.getFloat(DecodeThread.BARCODE_SCALED_FACTOR); + } + activity.handleDecode((Result) message.obj, barcode, scaleFactor); + break; + case R.id.decode_failed: + // We're decoding as fast as possible, so when one decode fails, start another. + state = State.PREVIEW; + cameraManager.requestPreviewFrame(decodeThread.getHandler(), R.id.decode); + break; + case R.id.return_scan_result: + activity.setResult(Activity.RESULT_OK, (Intent) message.obj); + activity.finish(); + break; + case R.id.launch_product_query: + String url = (String) message.obj; + + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + intent.setData(Uri.parse(url)); + + ResolveInfo resolveInfo = + activity.getPackageManager().resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY); + String browserPackageName = null; + if (resolveInfo != null && resolveInfo.activityInfo != null) { + browserPackageName = resolveInfo.activityInfo.packageName; + Log.d(TAG, "Using browser in package " + browserPackageName); + } + + // Needed for default Android browser / Chrome only apparently + if ("com.android.browser".equals(browserPackageName) || "com.android.chrome".equals(browserPackageName)) { + intent.setPackage(browserPackageName); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(Browser.EXTRA_APPLICATION_ID, browserPackageName); + } + + try { + activity.startActivity(intent); + } catch (ActivityNotFoundException ignored) { + Log.w(TAG, "Can't find anything to handle VIEW of URI " + url); + } + break; + } + } + + public void quitSynchronously() { + state = State.DONE; + cameraManager.stopPreview(); + Message quit = Message.obtain(decodeThread.getHandler(), R.id.quit); + quit.sendToTarget(); + try { + // Wait at most half a second; should be enough time, and onPause() will timeout quickly + decodeThread.join(500L); + } catch (InterruptedException e) { + // continue + } + + // Be absolutely sure we don't send any queued up messages + removeMessages(R.id.decode_succeeded); + removeMessages(R.id.decode_failed); + } + + private void restartPreviewAndDecode() { + if (state == State.SUCCESS) { + state = State.PREVIEW; + cameraManager.requestPreviewFrame(decodeThread.getHandler(), R.id.decode); + activity.drawViewfinder(); + } + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/Contents.java b/app/src/main/java/com/google/zxing/client/android/Contents.java new file mode 100644 index 0000000..1cfff17 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/Contents.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android; + +import android.provider.ContactsContract; + +/** + * The set of constants to use when sending Barcode Scanner an Intent which requests a barcode + * to be encoded. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class Contents { + private Contents() { + } + + public static final class Type { + /** + * Plain text. Use Intent.putExtra(DATA, string). This can be used for URLs too, but string + * must include "http://" or "https://". + */ + public static final String TEXT = "TEXT_TYPE"; + + /** + * An email type. Use Intent.putExtra(DATA, string) where string is the email address. + */ + public static final String EMAIL = "EMAIL_TYPE"; + + /** + * Use Intent.putExtra(DATA, string) where string is the phone number to call. + */ + public static final String PHONE = "PHONE_TYPE"; + + /** + * An SMS type. Use Intent.putExtra(DATA, string) where string is the number to SMS. + */ + public static final String SMS = "SMS_TYPE"; + + /** + * A contact. Send a request to encode it as follows: + * {@code + * import android.provider.Contacts; + * + * Intent intent = new Intent(Intents.Encode.ACTION); + * intent.putExtra(Intents.Encode.TYPE, CONTACT); + * Bundle bundle = new Bundle(); + * bundle.putString(ContactsContract.Intents.Insert.NAME, "Jenny"); + * bundle.putString(ContactsContract.Intents.Insert.PHONE, "8675309"); + * bundle.putString(ContactsContract.Intents.Insert.EMAIL, "jenny@the80s.com"); + * bundle.putString(ContactsContract.Intents.Insert.POSTAL, "123 Fake St. San Francisco, CA 94102"); + * intent.putExtra(Intents.Encode.DATA, bundle); + * } + */ + public static final String CONTACT = "CONTACT_TYPE"; + + /** + * A geographic location. Use as follows: + * Bundle bundle = new Bundle(); + * bundle.putFloat("LAT", latitude); + * bundle.putFloat("LONG", longitude); + * intent.putExtra(Intents.Encode.DATA, bundle); + */ + public static final String LOCATION = "LOCATION_TYPE"; + + private Type() { + } + } + + public static final String URL_KEY = "URL_KEY"; + + public static final String NOTE_KEY = "NOTE_KEY"; + + /** + * When using Type.CONTACT, these arrays provide the keys for adding or retrieving multiple + * phone numbers and addresses. + */ + public static final String[] PHONE_KEYS = { + ContactsContract.Intents.Insert.PHONE, + ContactsContract.Intents.Insert.SECONDARY_PHONE, + ContactsContract.Intents.Insert.TERTIARY_PHONE + }; + + public static final String[] PHONE_TYPE_KEYS = { + ContactsContract.Intents.Insert.PHONE_TYPE, + ContactsContract.Intents.Insert.SECONDARY_PHONE_TYPE, + ContactsContract.Intents.Insert.TERTIARY_PHONE_TYPE + }; + + public static final String[] EMAIL_KEYS = { + ContactsContract.Intents.Insert.EMAIL, + ContactsContract.Intents.Insert.SECONDARY_EMAIL, + ContactsContract.Intents.Insert.TERTIARY_EMAIL + }; + + public static final String[] EMAIL_TYPE_KEYS = { + ContactsContract.Intents.Insert.EMAIL_TYPE, + ContactsContract.Intents.Insert.SECONDARY_EMAIL_TYPE, + ContactsContract.Intents.Insert.TERTIARY_EMAIL_TYPE + }; + +} diff --git a/app/src/main/java/com/google/zxing/client/android/DecodeFormatManager.java b/app/src/main/java/com/google/zxing/client/android/DecodeFormatManager.java new file mode 100644 index 0000000..0cef607 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/DecodeFormatManager.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android; + +import android.content.Intent; +import android.net.Uri; +import com.google.zxing.BarcodeFormat; + +import java.util.Arrays; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +final class DecodeFormatManager { + + private static final Pattern COMMA_PATTERN = Pattern.compile(","); + + static final Set PRODUCT_FORMATS; + static final Set INDUSTRIAL_FORMATS; + private static final Set ONE_D_FORMATS; + static final Set QR_CODE_FORMATS = EnumSet.of(BarcodeFormat.QR_CODE); + static final Set DATA_MATRIX_FORMATS = EnumSet.of(BarcodeFormat.DATA_MATRIX); + static final Set AZTEC_FORMATS = EnumSet.of(BarcodeFormat.AZTEC); + static final Set PDF417_FORMATS = EnumSet.of(BarcodeFormat.PDF_417); + static { + PRODUCT_FORMATS = EnumSet.of(BarcodeFormat.UPC_A, + BarcodeFormat.UPC_E, + BarcodeFormat.EAN_13, + BarcodeFormat.EAN_8, + BarcodeFormat.RSS_14, + BarcodeFormat.RSS_EXPANDED); + INDUSTRIAL_FORMATS = EnumSet.of(BarcodeFormat.CODE_39, + BarcodeFormat.CODE_93, + BarcodeFormat.CODE_128, + BarcodeFormat.ITF, + BarcodeFormat.CODABAR); + ONE_D_FORMATS = EnumSet.copyOf(PRODUCT_FORMATS); + ONE_D_FORMATS.addAll(INDUSTRIAL_FORMATS); + } + private static final Map> FORMATS_FOR_MODE; + static { + FORMATS_FOR_MODE = new HashMap<>(); + FORMATS_FOR_MODE.put(Intents.Scan.ONE_D_MODE, ONE_D_FORMATS); + FORMATS_FOR_MODE.put(Intents.Scan.PRODUCT_MODE, PRODUCT_FORMATS); + FORMATS_FOR_MODE.put(Intents.Scan.QR_CODE_MODE, QR_CODE_FORMATS); + FORMATS_FOR_MODE.put(Intents.Scan.DATA_MATRIX_MODE, DATA_MATRIX_FORMATS); + FORMATS_FOR_MODE.put(Intents.Scan.AZTEC_MODE, AZTEC_FORMATS); + FORMATS_FOR_MODE.put(Intents.Scan.PDF417_MODE, PDF417_FORMATS); + } + + private DecodeFormatManager() {} + + static Set parseDecodeFormats(Intent intent) { + Iterable scanFormats = null; + CharSequence scanFormatsString = intent.getStringExtra(Intents.Scan.FORMATS); + if (scanFormatsString != null) { + scanFormats = Arrays.asList(COMMA_PATTERN.split(scanFormatsString)); + } + return parseDecodeFormats(scanFormats, intent.getStringExtra(Intents.Scan.MODE)); + } + + static Set parseDecodeFormats(Uri inputUri) { + List formats = inputUri.getQueryParameters(Intents.Scan.FORMATS); + if (formats != null && formats.size() == 1 && formats.get(0) != null){ + formats = Arrays.asList(COMMA_PATTERN.split(formats.get(0))); + } + return parseDecodeFormats(formats, inputUri.getQueryParameter(Intents.Scan.MODE)); + } + + private static Set parseDecodeFormats(Iterable scanFormats, String decodeMode) { + if (scanFormats != null) { + Set formats = EnumSet.noneOf(BarcodeFormat.class); + try { + for (String format : scanFormats) { + formats.add(BarcodeFormat.valueOf(format)); + } + return formats; + } catch (IllegalArgumentException iae) { + // ignore it then + } + } + if (decodeMode != null) { + return FORMATS_FOR_MODE.get(decodeMode); + } + return null; + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/DecodeHandler.java b/app/src/main/java/com/google/zxing/client/android/DecodeHandler.java new file mode 100644 index 0000000..1a8dc9c --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/DecodeHandler.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android; + +import android.graphics.Bitmap; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; + +import com.google.zxing.BinaryBitmap; +import com.google.zxing.DecodeHintType; +import com.google.zxing.MultiFormatReader; +import com.google.zxing.PlanarYUVLuminanceSource; +import com.google.zxing.ReaderException; +import com.google.zxing.Result; +import com.google.zxing.common.HybridBinarizer; + +import net.foucry.pilldroid.R; + +import java.io.ByteArrayOutputStream; +import java.util.Map; + +final class DecodeHandler extends Handler { + + private static final String TAG = DecodeHandler.class.getSimpleName(); + + private final CaptureActivity activity; + private final MultiFormatReader multiFormatReader; + private boolean running = true; + + DecodeHandler(CaptureActivity activity, Map hints) { + multiFormatReader = new MultiFormatReader(); + multiFormatReader.setHints(hints); + this.activity = activity; + } + + @Override + public void handleMessage(Message message) { + if (!running) { + return; + } + switch (message.what) { + case R.id.decode: + decode((byte[]) message.obj, message.arg1, message.arg2); + break; + case R.id.quit: + running = false; + Looper.myLooper().quit(); + break; + } + } + + /** + * Decode the data within the viewfinder rectangle, and time how long it took. For efficiency, + * reuse the same reader objects from one decode to the next. + * + * @param data The YUV preview frame. + * @param width The width of the preview frame. + * @param height The height of the preview frame. + */ + private void decode(byte[] data, int width, int height) { + long start = System.currentTimeMillis(); + Result rawResult = null; + PlanarYUVLuminanceSource source = activity.getCameraManager().buildLuminanceSource(data, width, height); + if (source != null) { + BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); + try { + rawResult = multiFormatReader.decodeWithState(bitmap); + } catch (ReaderException re) { + // continue + } finally { + multiFormatReader.reset(); + } + } + + Handler handler = activity.getHandler(); + if (rawResult != null) { + // Don't log the barcode contents for security. + long end = System.currentTimeMillis(); + Log.d(TAG, "Found barcode in " + (end - start) + " ms"); + if (handler != null) { + Message message = Message.obtain(handler, R.id.decode_succeeded, rawResult); + Bundle bundle = new Bundle(); + bundleThumbnail(source, bundle); + message.setData(bundle); + message.sendToTarget(); + } + } else { + if (handler != null) { + Message message = Message.obtain(handler, R.id.decode_failed); + message.sendToTarget(); + } + } + } + + private static void bundleThumbnail(PlanarYUVLuminanceSource source, Bundle bundle) { + int[] pixels = source.renderThumbnail(); + int width = source.getThumbnailWidth(); + int height = source.getThumbnailHeight(); + Bitmap bitmap = Bitmap.createBitmap(pixels, 0, width, width, height, Bitmap.Config.ARGB_8888); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.JPEG, 50, out); + bundle.putByteArray(DecodeThread.BARCODE_BITMAP, out.toByteArray()); + bundle.putFloat(DecodeThread.BARCODE_SCALED_FACTOR, (float) width / source.getWidth()); + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/DecodeHintManager.java b/app/src/main/java/com/google/zxing/client/android/DecodeHintManager.java new file mode 100644 index 0000000..39305fe --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/DecodeHintManager.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2013 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android; + +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Pattern; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; + +import com.google.zxing.DecodeHintType; + +/** + * @author Lachezar Dobrev + */ +final class DecodeHintManager { + + private static final String TAG = DecodeHintManager.class.getSimpleName(); + + // This pattern is used in decoding integer arrays. + private static final Pattern COMMA = Pattern.compile(","); + + private DecodeHintManager() {} + + /** + *

Split a query string into a list of name-value pairs.

+ * + *

This is an alternative to the {@link Uri#getQueryParameterNames()} and + * {@link Uri#getQueryParameters(String)}, which are quirky and not suitable + * for exist-only Uri parameters.

+ * + *

This method ignores multiple parameters with the same name and returns the + * first one only. This is technically incorrect, but should be acceptable due + * to the method of processing Hints: no multiple values for a hint.

+ * + * @param query query to split + * @return name-value pairs + */ + private static Map splitQuery(String query) { + Map map = new HashMap<>(); + int pos = 0; + while (pos < query.length()) { + if (query.charAt(pos) == '&') { + // Skip consecutive ampersand separators. + pos ++; + continue; + } + int amp = query.indexOf('&', pos); + int equ = query.indexOf('=', pos); + if (amp < 0) { + // This is the last element in the query, no more ampersand elements. + String name; + String text; + if (equ < 0) { + // No equal sign + name = query.substring(pos); + name = name.replace('+', ' '); // Preemptively decode + + name = Uri.decode(name); + text = ""; + } else { + // Split name and text. + name = query.substring(pos, equ); + name = name.replace('+', ' '); // Preemptively decode + + name = Uri.decode(name); + text = query.substring(equ + 1); + text = text.replace('+', ' '); // Preemptively decode + + text = Uri.decode(text); + } + if (!map.containsKey(name)) { + map.put(name, text); + } + break; + } + if (equ < 0 || equ > amp) { + // No equal sign until the &: this is a simple parameter with no value. + String name = query.substring(pos, amp); + name = name.replace('+', ' '); // Preemptively decode + + name = Uri.decode(name); + if (!map.containsKey(name)) { + map.put(name, ""); + } + pos = amp + 1; + continue; + } + String name = query.substring(pos, equ); + name = name.replace('+', ' '); // Preemptively decode + + name = Uri.decode(name); + String text = query.substring(equ+1, amp); + text = text.replace('+', ' '); // Preemptively decode + + text = Uri.decode(text); + if (!map.containsKey(name)) { + map.put(name, text); + } + pos = amp + 1; + } + return map; + } + + static Map parseDecodeHints(Uri inputUri) { + String query = inputUri.getEncodedQuery(); + if (query == null || query.isEmpty()) { + return null; + } + + // Extract parameters + Map parameters = splitQuery(query); + + Map hints = new EnumMap<>(DecodeHintType.class); + + for (DecodeHintType hintType: DecodeHintType.values()) { + + if (hintType == DecodeHintType.CHARACTER_SET || + hintType == DecodeHintType.NEED_RESULT_POINT_CALLBACK || + hintType == DecodeHintType.POSSIBLE_FORMATS) { + continue; // This hint is specified in another way + } + + String parameterName = hintType.name(); + String parameterText = parameters.get(parameterName); + if (parameterText == null) { + continue; + } + if (hintType.getValueType().equals(Object.class)) { + // This is an unspecified type of hint content. Use the value as is. + // TODO: Can we make a different assumption on this? + hints.put(hintType, parameterText); + continue; + } + if (hintType.getValueType().equals(Void.class)) { + // Void hints are just flags: use the constant specified by DecodeHintType + hints.put(hintType, Boolean.TRUE); + continue; + } + if (hintType.getValueType().equals(String.class)) { + // A string hint: use the decoded value. + hints.put(hintType, parameterText); + continue; + } + if (hintType.getValueType().equals(Boolean.class)) { + // A boolean hint: a few values for false, everything else is true. + // An empty parameter is simply a flag-style parameter, assuming true + if (parameterText.isEmpty()) { + hints.put(hintType, Boolean.TRUE); + } else if ("0".equals(parameterText) || + "false".equalsIgnoreCase(parameterText) || + "no".equalsIgnoreCase(parameterText)) { + hints.put(hintType, Boolean.FALSE); + } else { + hints.put(hintType, Boolean.TRUE); + } + + continue; + } + if (hintType.getValueType().equals(int[].class)) { + // An integer array. Used to specify valid lengths. + // Strip a trailing comma as in Java style array initialisers. + if (!parameterText.isEmpty() && parameterText.charAt(parameterText.length() - 1) == ',') { + parameterText = parameterText.substring(0, parameterText.length() - 1); + } + String[] values = COMMA.split(parameterText); + int[] array = new int[values.length]; + for (int i = 0; i < values.length; i++) { + try { + array[i] = Integer.parseInt(values[i]); + } catch (NumberFormatException ignored) { + Log.w(TAG, "Skipping array of integers hint " + hintType + " due to invalid numeric value: '" + values[i] + '\''); + array = null; + break; + } + } + if (array != null) { + hints.put(hintType, array); + } + continue; + } + Log.w(TAG, "Unsupported hint type '" + hintType + "' of type " + hintType.getValueType()); + } + + Log.i(TAG, "Hints from the URI: " + hints); + return hints; + } + + static Map parseDecodeHints(Intent intent) { + Bundle extras = intent.getExtras(); + if (extras == null || extras.isEmpty()) { + return null; + } + Map hints = new EnumMap<>(DecodeHintType.class); + + for (DecodeHintType hintType: DecodeHintType.values()) { + + if (hintType == DecodeHintType.CHARACTER_SET || + hintType == DecodeHintType.NEED_RESULT_POINT_CALLBACK || + hintType == DecodeHintType.POSSIBLE_FORMATS) { + continue; // This hint is specified in another way + } + + String hintName = hintType.name(); + if (extras.containsKey(hintName)) { + if (hintType.getValueType().equals(Void.class)) { + // Void hints are just flags: use the constant specified by the DecodeHintType + hints.put(hintType, Boolean.TRUE); + } else { + Object hintData = extras.get(hintName); + if (hintType.getValueType().isInstance(hintData)) { + hints.put(hintType, hintData); + } else { + Log.w(TAG, "Ignoring hint " + hintType + " because it is not assignable from " + hintData); + } + } + } + } + + Log.i(TAG, "Hints from the Intent: " + hints); + return hints; + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/DecodeThread.java b/app/src/main/java/com/google/zxing/client/android/DecodeThread.java new file mode 100644 index 0000000..fab4039 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/DecodeThread.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.DecodeHintType; +import com.google.zxing.ResultPointCallback; + +import android.content.SharedPreferences; +import android.os.Handler; +import android.os.Looper; +import android.preference.PreferenceManager; +import android.util.Log; + +import java.util.Collection; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.Map; +import java.util.concurrent.CountDownLatch; + +/** + * This thread does all the heavy lifting of decoding the images. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +final class DecodeThread extends Thread { + + public static final String BARCODE_BITMAP = "barcode_bitmap"; + public static final String BARCODE_SCALED_FACTOR = "barcode_scaled_factor"; + + private final CaptureActivity activity; + private final Map hints; + private Handler handler; + private final CountDownLatch handlerInitLatch; + + DecodeThread(CaptureActivity activity, + Collection decodeFormats, + Map baseHints, + String characterSet, + ResultPointCallback resultPointCallback) { + + this.activity = activity; + handlerInitLatch = new CountDownLatch(1); + + hints = new EnumMap<>(DecodeHintType.class); + if (baseHints != null) { + hints.putAll(baseHints); + } + + // The prefs can't change while the thread is running, so pick them up once here. + if (decodeFormats == null || decodeFormats.isEmpty()) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity); + decodeFormats = EnumSet.noneOf(BarcodeFormat.class); + if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_1D_PRODUCT, true)) { + decodeFormats.addAll(DecodeFormatManager.PRODUCT_FORMATS); + } + if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_1D_INDUSTRIAL, true)) { + decodeFormats.addAll(DecodeFormatManager.INDUSTRIAL_FORMATS); + } + if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_QR, true)) { + decodeFormats.addAll(DecodeFormatManager.QR_CODE_FORMATS); + } + if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_DATA_MATRIX, true)) { + decodeFormats.addAll(DecodeFormatManager.DATA_MATRIX_FORMATS); + } + if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_AZTEC, false)) { + decodeFormats.addAll(DecodeFormatManager.AZTEC_FORMATS); + } + if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_PDF417, false)) { + decodeFormats.addAll(DecodeFormatManager.PDF417_FORMATS); + } + } + hints.put(DecodeHintType.POSSIBLE_FORMATS, decodeFormats); + + if (characterSet != null) { + hints.put(DecodeHintType.CHARACTER_SET, characterSet); + } + hints.put(DecodeHintType.NEED_RESULT_POINT_CALLBACK, resultPointCallback); + Log.i("DecodeThread", "Hints: " + hints); + } + + Handler getHandler() { + try { + handlerInitLatch.await(); + } catch (InterruptedException ie) { + // continue? + } + return handler; + } + + @Override + public void run() { + Looper.prepare(); + handler = new DecodeHandler(activity, hints); + handlerInitLatch.countDown(); + Looper.loop(); + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/FinishListener.java b/app/src/main/java/com/google/zxing/client/android/FinishListener.java new file mode 100644 index 0000000..5d59886 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/FinishListener.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android; + +import android.app.Activity; +import android.content.DialogInterface; + +/** + * Simple listener used to exit the app in a few cases. + * + * @author Sean Owen + */ +public final class FinishListener implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener { + + private final Activity activityToFinish; + + public FinishListener(Activity activityToFinish) { + this.activityToFinish = activityToFinish; + } + + @Override + public void onCancel(DialogInterface dialogInterface) { + run(); + } + + @Override + public void onClick(DialogInterface dialogInterface, int i) { + run(); + } + + private void run() { + activityToFinish.finish(); + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/HelpActivity.java b/app/src/main/java/com/google/zxing/client/android/HelpActivity.java new file mode 100644 index 0000000..1f964bd --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/HelpActivity.java @@ -0,0 +1,66 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android; + +import android.app.Activity; +import android.os.Bundle; +import android.view.KeyEvent; +import android.webkit.WebView; + +import net.foucry.pilldroid.R; + +/** + * An HTML-based help screen. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class HelpActivity extends Activity { + + private static final String BASE_URL = + "file:///android_asset/html-" + LocaleManager.getTranslatedAssetLanguage() + '/'; + + private WebView webView; + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + setContentView(R.layout.help); + + webView = (WebView) findViewById(R.id.help_contents); + + if (icicle == null) { + webView.loadUrl(BASE_URL + "index.html"); + } else { + webView.restoreState(icicle); + } + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK && webView.canGoBack()) { + webView.goBack(); + return true; + } + return super.onKeyDown(keyCode, event); + } + + @Override + protected void onSaveInstanceState(Bundle icicle) { + super.onSaveInstanceState(icicle); + webView.saveState(icicle); + } +} diff --git a/app/src/main/java/com/google/zxing/client/android/HttpHelper.java b/app/src/main/java/com/google/zxing/client/android/HttpHelper.java new file mode 100644 index 0000000..5a1543d --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/HttpHelper.java @@ -0,0 +1,228 @@ +/* + * Copyright 2011 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android; + +import android.util.Log; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; + +/** + * Utility methods for retrieving content over HTTP using the more-supported {@code java.net} classes + * in Android. + */ +public final class HttpHelper { + + private static final String TAG = HttpHelper.class.getSimpleName(); + + private static final Collection REDIRECTOR_DOMAINS = new HashSet<>(Arrays.asList( + "amzn.to", "bit.ly", "bitly.com", "fb.me", "goo.gl", "is.gd", "j.mp", "lnkd.in", "ow.ly", + "R.BEETAGG.COM", "r.beetagg.com", "SCN.BY", "su.pr", "t.co", "tinyurl.com", "tr.im" + )); + + private HttpHelper() { + } + + public enum ContentType { + /** HTML-like content type, including HTML, XHTML, etc. */ + HTML, + /** JSON content */ + JSON, + /** XML */ + XML, + /** Plain text content */ + TEXT, + } + + /** + * Downloads the entire resource instead of part. + * + * @param uri URI to retrieve + * @param type expected text-like MIME type of that content + * @return content as a {@code String} + * @throws IOException if the content can't be retrieved because of a bad URI, network problem, etc. + * @see #downloadViaHttp(String, HttpHelper.ContentType, int) + */ + public static CharSequence downloadViaHttp(String uri, ContentType type) throws IOException { + return downloadViaHttp(uri, type, Integer.MAX_VALUE); + } + + /** + * @param uri URI to retrieve + * @param type expected text-like MIME type of that content + * @param maxChars approximate maximum characters to read from the source + * @return content as a {@code String} + * @throws IOException if the content can't be retrieved because of a bad URI, network problem, etc. + */ + public static CharSequence downloadViaHttp(String uri, ContentType type, int maxChars) throws IOException { + String contentTypes; + switch (type) { + case HTML: + contentTypes = "application/xhtml+xml,text/html,text/*,*/*"; + break; + case JSON: + contentTypes = "application/json,text/*,*/*"; + break; + case XML: + contentTypes = "application/xml,text/*,*/*"; + break; + case TEXT: + default: + contentTypes = "text/*,*/*"; + } + return downloadViaHttp(uri, contentTypes, maxChars); + } + + private static CharSequence downloadViaHttp(String uri, String contentTypes, int maxChars) throws IOException { + int redirects = 0; + while (redirects < 5) { + URL url = new URL(uri); + HttpURLConnection connection = safelyOpenConnection(url); + connection.setInstanceFollowRedirects(true); // Won't work HTTP -> HTTPS or vice versa + connection.setRequestProperty("Accept", contentTypes); + connection.setRequestProperty("Accept-Charset", "utf-8,*"); + connection.setRequestProperty("User-Agent", "ZXing (Android)"); + try { + int responseCode = safelyConnect(connection); + switch (responseCode) { + case HttpURLConnection.HTTP_OK: + return consume(connection, maxChars); + case HttpURLConnection.HTTP_MOVED_TEMP: + String location = connection.getHeaderField("Location"); + if (location != null) { + uri = location; + redirects++; + continue; + } + throw new IOException("No Location"); + default: + throw new IOException("Bad HTTP response: " + responseCode); + } + } finally { + connection.disconnect(); + } + } + throw new IOException("Too many redirects"); + } + + private static String getEncoding(URLConnection connection) { + String contentTypeHeader = connection.getHeaderField("Content-Type"); + if (contentTypeHeader != null) { + int charsetStart = contentTypeHeader.indexOf("charset="); + if (charsetStart >= 0) { + return contentTypeHeader.substring(charsetStart + "charset=".length()); + } + } + return "UTF-8"; + } + + private static CharSequence consume(URLConnection connection, int maxChars) throws IOException { + String encoding = getEncoding(connection); + StringBuilder out = new StringBuilder(); + Reader in = null; + try { + in = new InputStreamReader(connection.getInputStream(), encoding); + char[] buffer = new char[1024]; + int charsRead; + while (out.length() < maxChars && (charsRead = in.read(buffer)) > 0) { + out.append(buffer, 0, charsRead); + } + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException | NullPointerException ioe) { + // continue + } + } + } + return out; + } + + public static URI unredirect(URI uri) throws IOException { + if (!REDIRECTOR_DOMAINS.contains(uri.getHost())) { + return uri; + } + URL url = uri.toURL(); + HttpURLConnection connection = safelyOpenConnection(url); + connection.setInstanceFollowRedirects(false); + connection.setDoInput(false); + connection.setRequestMethod("HEAD"); + connection.setRequestProperty("User-Agent", "ZXing (Android)"); + try { + int responseCode = safelyConnect(connection); + switch (responseCode) { + case HttpURLConnection.HTTP_MULT_CHOICE: + case HttpURLConnection.HTTP_MOVED_PERM: + case HttpURLConnection.HTTP_MOVED_TEMP: + case HttpURLConnection.HTTP_SEE_OTHER: + case 307: // No constant for 307 Temporary Redirect ? + String location = connection.getHeaderField("Location"); + if (location != null) { + try { + return new URI(location); + } catch (URISyntaxException e) { + // nevermind + } + } + } + return uri; + } finally { + connection.disconnect(); + } + } + + private static HttpURLConnection safelyOpenConnection(URL url) throws IOException { + URLConnection conn; + try { + conn = url.openConnection(); + } catch (NullPointerException npe) { + // Another strange bug in Android? + Log.w(TAG, "Bad URI? " + url); + throw new IOException(npe); + } + if (!(conn instanceof HttpURLConnection)) { + throw new IOException(); + } + return (HttpURLConnection) conn; + } + + private static int safelyConnect(HttpURLConnection connection) throws IOException { + try { + connection.connect(); + } catch (NullPointerException | IllegalArgumentException | IndexOutOfBoundsException | SecurityException e) { + // this is an Android bug: http://code.google.com/p/android/issues/detail?id=16895 + throw new IOException(e); + } + try { + return connection.getResponseCode(); + } catch (NullPointerException | StringIndexOutOfBoundsException | IllegalArgumentException e) { + // this is maybe this Android bug: http://code.google.com/p/android/issues/detail?id=15554 + throw new IOException(e); + } + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/InactivityTimer.java b/app/src/main/java/com/google/zxing/client/android/InactivityTimer.java new file mode 100644 index 0000000..5e33d90 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/InactivityTimer.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.AsyncTask; +import android.os.BatteryManager; +import android.util.Log; + +/** + * Finishes an activity after a period of inactivity if the device is on battery power. + */ +final class InactivityTimer { + + private static final String TAG = InactivityTimer.class.getSimpleName(); + + private static final long INACTIVITY_DELAY_MS = 5 * 60 * 1000L; + + private final Activity activity; + private final BroadcastReceiver powerStatusReceiver; + private boolean registered; + private AsyncTask inactivityTask; + + InactivityTimer(Activity activity) { + this.activity = activity; + powerStatusReceiver = new PowerStatusReceiver(); + registered = false; + onActivity(); + } + + synchronized void onActivity() { + cancel(); + inactivityTask = new InactivityAsyncTask(); + inactivityTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + public synchronized void onPause() { + cancel(); + if (registered) { + activity.unregisterReceiver(powerStatusReceiver); + registered = false; + } else { + Log.w(TAG, "PowerStatusReceiver was never registered?"); + } + } + + public synchronized void onResume() { + if (registered) { + Log.w(TAG, "PowerStatusReceiver was already registered?"); + } else { + activity.registerReceiver(powerStatusReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + registered = true; + } + onActivity(); + } + + private synchronized void cancel() { + AsyncTask task = inactivityTask; + if (task != null) { + task.cancel(true); + inactivityTask = null; + } + } + + void shutdown() { + cancel(); + } + + private final class PowerStatusReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent){ + if (Intent.ACTION_BATTERY_CHANGED.equals(intent.getAction())) { + // 0 indicates that we're on battery + boolean onBatteryNow = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) <= 0; + if (onBatteryNow) { + InactivityTimer.this.onActivity(); + } else { + InactivityTimer.this.cancel(); + } + } + } + } + + private final class InactivityAsyncTask extends AsyncTask { + @Override + protected Object doInBackground(Object... objects) { + try { + Thread.sleep(INACTIVITY_DELAY_MS); + Log.i(TAG, "Finishing activity due to inactivity"); + activity.finish(); + } catch (InterruptedException e) { + // continue without killing + } + return null; + } + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/IntentSource.java b/app/src/main/java/com/google/zxing/client/android/IntentSource.java new file mode 100644 index 0000000..3222db6 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/IntentSource.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2011 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android; + +enum IntentSource { + + NATIVE_APP_INTENT, + PRODUCT_SEARCH_LINK, + ZXING_LINK, + NONE + +} diff --git a/app/src/main/java/com/google/zxing/client/android/Intents.java b/app/src/main/java/com/google/zxing/client/android/Intents.java new file mode 100644 index 0000000..6e59e80 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/Intents.java @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android; + +/** + * This class provides the constants to use when sending an Intent to Barcode Scanner. + * These strings are effectively API and cannot be changed. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class Intents { + private Intents() { + } + + public static final class Scan { + /** + * Send this intent to open the Barcodes app in scanning mode, find a barcode, and return + * the results. + */ + public static final String ACTION = "com.google.zxing.client.android.SCAN"; + + /** + * By default, sending this will decode all barcodes that we understand. However it + * may be useful to limit scanning to certain formats. Use + * {@link android.content.Intent#putExtra(String, String)} with one of the values below. + * + * Setting this is effectively shorthand for setting explicit formats with {@link #FORMATS}. + * It is overridden by that setting. + */ + public static final String MODE = "SCAN_MODE"; + + /** + * Decode only UPC and EAN barcodes. This is the right choice for shopping apps which get + * prices, reviews, etc. for products. + */ + public static final String PRODUCT_MODE = "PRODUCT_MODE"; + + /** + * Decode only 1D barcodes. + */ + public static final String ONE_D_MODE = "ONE_D_MODE"; + + /** + * Decode only QR codes. + */ + public static final String QR_CODE_MODE = "QR_CODE_MODE"; + + /** + * Decode only Data Matrix codes. + */ + public static final String DATA_MATRIX_MODE = "DATA_MATRIX_MODE"; + + /** + * Decode only Aztec. + */ + public static final String AZTEC_MODE = "AZTEC_MODE"; + + /** + * Decode only PDF417. + */ + public static final String PDF417_MODE = "PDF417_MODE"; + + /** + * Comma-separated list of formats to scan for. The values must match the names of + * {@link com.google.zxing.BarcodeFormat}s, e.g. {@link com.google.zxing.BarcodeFormat#EAN_13}. + * Example: "EAN_13,EAN_8,QR_CODE". This overrides {@link #MODE}. + */ + public static final String FORMATS = "SCAN_FORMATS"; + + /** + * Optional parameter to specify the id of the camera from which to recognize barcodes. + * Overrides the default camera that would otherwise would have been selected. + * If provided, should be an int. + */ + public static final String CAMERA_ID = "SCAN_CAMERA_ID"; + + /** + * @see com.google.zxing.DecodeHintType#CHARACTER_SET + */ + public static final String CHARACTER_SET = "CHARACTER_SET"; + + /** + * Optional parameters to specify the width and height of the scanning rectangle in pixels. + * The app will try to honor these, but will clamp them to the size of the preview frame. + * You should specify both or neither, and pass the size as an int. + */ + public static final String WIDTH = "SCAN_WIDTH"; + public static final String HEIGHT = "SCAN_HEIGHT"; + + /** + * Desired duration in milliseconds for which to pause after a successful scan before + * returning to the calling intent. Specified as a long, not an integer! + * For example: 1000L, not 1000. + */ + public static final String RESULT_DISPLAY_DURATION_MS = "RESULT_DISPLAY_DURATION_MS"; + + /** + * Prompt to show on-screen when scanning by intent. Specified as a {@link String}. + */ + public static final String PROMPT_MESSAGE = "PROMPT_MESSAGE"; + + /** + * If a barcode is found, Barcodes returns {@link android.app.Activity#RESULT_OK} to + * {@link android.app.Activity#onActivityResult(int, int, android.content.Intent)} + * of the app which requested the scan via + * {@link android.app.Activity#startActivityForResult(android.content.Intent, int)} + * The barcodes contents can be retrieved with + * {@link android.content.Intent#getStringExtra(String)}. + * If the user presses Back, the result code will be {@link android.app.Activity#RESULT_CANCELED}. + */ + public static final String RESULT = "SCAN_RESULT"; + + /** + * Call {@link android.content.Intent#getStringExtra(String)} with {@link #RESULT_FORMAT} + * to determine which barcode format was found. + * See {@link com.google.zxing.BarcodeFormat} for possible values. + */ + public static final String RESULT_FORMAT = "SCAN_RESULT_FORMAT"; + + /** + * Call {@link android.content.Intent#getStringExtra(String)} with {@link #RESULT_UPC_EAN_EXTENSION} + * to return the content of any UPC extension barcode that was also found. Only applicable + * to {@link com.google.zxing.BarcodeFormat#UPC_A} and {@link com.google.zxing.BarcodeFormat#EAN_13} + * formats. + */ + public static final String RESULT_UPC_EAN_EXTENSION = "SCAN_RESULT_UPC_EAN_EXTENSION"; + + /** + * Call {@link android.content.Intent#getByteArrayExtra(String)} with {@link #RESULT_BYTES} + * to get a {@code byte[]} of raw bytes in the barcode, if available. + */ + public static final String RESULT_BYTES = "SCAN_RESULT_BYTES"; + + /** + * Key for the value of {@link com.google.zxing.ResultMetadataType#ORIENTATION}, if available. + * Call {@link android.content.Intent#getIntArrayExtra(String)} with {@link #RESULT_ORIENTATION}. + */ + public static final String RESULT_ORIENTATION = "SCAN_RESULT_ORIENTATION"; + + /** + * Key for the value of {@link com.google.zxing.ResultMetadataType#ERROR_CORRECTION_LEVEL}, if available. + * Call {@link android.content.Intent#getStringExtra(String)} with {@link #RESULT_ERROR_CORRECTION_LEVEL}. + */ + public static final String RESULT_ERROR_CORRECTION_LEVEL = "SCAN_RESULT_ERROR_CORRECTION_LEVEL"; + + /** + * Prefix for keys that map to the values of {@link com.google.zxing.ResultMetadataType#BYTE_SEGMENTS}, + * if available. The actual values will be set under a series of keys formed by adding 0, 1, 2, ... + * to this prefix. So the first byte segment is under key "SCAN_RESULT_BYTE_SEGMENTS_0" for example. + * Call {@link android.content.Intent#getByteArrayExtra(String)} with these keys. + */ + public static final String RESULT_BYTE_SEGMENTS_PREFIX = "SCAN_RESULT_BYTE_SEGMENTS_"; + + /** + * Setting this to false will not save scanned codes in the history. Specified as a {@code boolean}. + */ + public static final String SAVE_HISTORY = "SAVE_HISTORY"; + + private Scan() { + } + } + + public static final class History { + + public static final String ITEM_NUMBER = "ITEM_NUMBER"; + + private History() { + } + } + + public static final class Encode { + /** + * Send this intent to encode a piece of data as a QR code and display it full screen, so + * that another person can scan the barcode from your screen. + */ + public static final String ACTION = "com.google.zxing.client.android.ENCODE"; + + /** + * The data to encode. Use {@link android.content.Intent#putExtra(String, String)} or + * {@link android.content.Intent#putExtra(String, android.os.Bundle)}, + * depending on the type and format specified. Non-QR Code formats should + * just use a String here. For QR Code, see Contents for details. + */ + public static final String DATA = "ENCODE_DATA"; + + /** + * The type of data being supplied if the format is QR Code. Use + * {@link android.content.Intent#putExtra(String, String)} with one of {@link Contents.Type}. + */ + public static final String TYPE = "ENCODE_TYPE"; + + /** + * The barcode format to be displayed. If this isn't specified or is blank, + * it defaults to QR Code. Use {@link android.content.Intent#putExtra(String, String)}, where + * format is one of {@link com.google.zxing.BarcodeFormat}. + */ + public static final String FORMAT = "ENCODE_FORMAT"; + + /** + * Normally the contents of the barcode are displayed to the user in a TextView. Setting this + * boolean to false will hide that TextView, showing only the encode barcode. + */ + public static final String SHOW_CONTENTS = "ENCODE_SHOW_CONTENTS"; + + private Encode() { + } + } + + public static final class SearchBookContents { + /** + * Use Google Book Search to search the contents of the book provided. + */ + public static final String ACTION = "com.google.zxing.client.android.SEARCH_BOOK_CONTENTS"; + + /** + * The book to search, identified by ISBN number. + */ + public static final String ISBN = "ISBN"; + + /** + * An optional field which is the text to search for. + */ + public static final String QUERY = "QUERY"; + + private SearchBookContents() { + } + } + + public static final class WifiConnect { + /** + * Internal intent used to trigger connection to a wi-fi network. + */ + public static final String ACTION = "com.google.zxing.client.android.WIFI_CONNECT"; + + /** + * The network to connect to, all the configuration provided here. + */ + public static final String SSID = "SSID"; + + /** + * The network to connect to, all the configuration provided here. + */ + public static final String TYPE = "TYPE"; + + /** + * The network to connect to, all the configuration provided here. + */ + public static final String PASSWORD = "PASSWORD"; + + private WifiConnect() { + } + } + + public static final class Share { + /** + * Give the user a choice of items to encode as a barcode, then render it as a QR Code and + * display onscreen for a friend to scan with their phone. + */ + public static final String ACTION = "com.google.zxing.client.android.SHARE"; + + private Share() { + } + } +} diff --git a/app/src/main/java/com/google/zxing/client/android/LocaleManager.java b/app/src/main/java/com/google/zxing/client/android/LocaleManager.java new file mode 100644 index 0000000..e74f51d --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/LocaleManager.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Locale; +import java.util.Map; +import java.util.HashMap; + +/** + * Handles any locale-specific logic for the client. + * + * @author Sean Owen + */ +public final class LocaleManager { + + private static final String DEFAULT_TLD = "com"; + private static final String DEFAULT_COUNTRY = "US"; + private static final String DEFAULT_LANGUAGE = "en"; + + /** + * Locales (well, countries) where Google web search is available. + * These should be kept in sync with our translations. + */ + private static final Map GOOGLE_COUNTRY_TLD; + static { + GOOGLE_COUNTRY_TLD = new HashMap<>(); + GOOGLE_COUNTRY_TLD.put("AR", "com.ar"); // ARGENTINA + GOOGLE_COUNTRY_TLD.put("AU", "com.au"); // AUSTRALIA + GOOGLE_COUNTRY_TLD.put("BR", "com.br"); // BRAZIL + GOOGLE_COUNTRY_TLD.put("BG", "bg"); // BULGARIA + GOOGLE_COUNTRY_TLD.put(Locale.CANADA.getCountry(), "ca"); + GOOGLE_COUNTRY_TLD.put(Locale.CHINA.getCountry(), "cn"); + GOOGLE_COUNTRY_TLD.put("CZ", "cz"); // CZECH REPUBLIC + GOOGLE_COUNTRY_TLD.put("DK", "dk"); // DENMARK + GOOGLE_COUNTRY_TLD.put("FI", "fi"); // FINLAND + GOOGLE_COUNTRY_TLD.put(Locale.FRANCE.getCountry(), "fr"); + GOOGLE_COUNTRY_TLD.put(Locale.GERMANY.getCountry(), "de"); + GOOGLE_COUNTRY_TLD.put("GR", "gr"); // GREECE + GOOGLE_COUNTRY_TLD.put("HU", "hu"); // HUNGARY + GOOGLE_COUNTRY_TLD.put("ID", "co.id"); // INDONESIA + GOOGLE_COUNTRY_TLD.put("IL", "co.il"); // ISRAEL + GOOGLE_COUNTRY_TLD.put(Locale.ITALY.getCountry(), "it"); + GOOGLE_COUNTRY_TLD.put(Locale.JAPAN.getCountry(), "co.jp"); + GOOGLE_COUNTRY_TLD.put(Locale.KOREA.getCountry(), "co.kr"); + GOOGLE_COUNTRY_TLD.put("NL", "nl"); // NETHERLANDS + GOOGLE_COUNTRY_TLD.put("PL", "pl"); // POLAND + GOOGLE_COUNTRY_TLD.put("PT", "pt"); // PORTUGAL + GOOGLE_COUNTRY_TLD.put("RO", "ro"); // ROMANIA + GOOGLE_COUNTRY_TLD.put("RU", "ru"); // RUSSIA + GOOGLE_COUNTRY_TLD.put("SK", "sk"); // SLOVAK REPUBLIC + GOOGLE_COUNTRY_TLD.put("SI", "si"); // SLOVENIA + GOOGLE_COUNTRY_TLD.put("ES", "es"); // SPAIN + GOOGLE_COUNTRY_TLD.put("SE", "se"); // SWEDEN + GOOGLE_COUNTRY_TLD.put("CH", "ch"); // SWITZERLAND + GOOGLE_COUNTRY_TLD.put(Locale.TAIWAN.getCountry(), "tw"); + GOOGLE_COUNTRY_TLD.put("TR", "com.tr"); // TURKEY + GOOGLE_COUNTRY_TLD.put("UA", "com.ua"); // UKRAINE + GOOGLE_COUNTRY_TLD.put(Locale.UK.getCountry(), "co.uk"); + GOOGLE_COUNTRY_TLD.put(Locale.US.getCountry(), "com"); + } + + /** + * Google Product Search for mobile is available in fewer countries than web search. See here: + * http://support.google.com/merchants/bin/answer.py?hl=en-GB&answer=160619 + */ + private static final Map GOOGLE_PRODUCT_SEARCH_COUNTRY_TLD; + static { + GOOGLE_PRODUCT_SEARCH_COUNTRY_TLD = new HashMap<>(); + GOOGLE_PRODUCT_SEARCH_COUNTRY_TLD.put("AU", "com.au"); // AUSTRALIA + //GOOGLE_PRODUCT_SEARCH_COUNTRY_TLD.put(Locale.CHINA.getCountry(), "cn"); + GOOGLE_PRODUCT_SEARCH_COUNTRY_TLD.put(Locale.FRANCE.getCountry(), "fr"); + GOOGLE_PRODUCT_SEARCH_COUNTRY_TLD.put(Locale.GERMANY.getCountry(), "de"); + GOOGLE_PRODUCT_SEARCH_COUNTRY_TLD.put(Locale.ITALY.getCountry(), "it"); + GOOGLE_PRODUCT_SEARCH_COUNTRY_TLD.put(Locale.JAPAN.getCountry(), "co.jp"); + GOOGLE_PRODUCT_SEARCH_COUNTRY_TLD.put("NL", "nl"); // NETHERLANDS + GOOGLE_PRODUCT_SEARCH_COUNTRY_TLD.put("ES", "es"); // SPAIN + GOOGLE_PRODUCT_SEARCH_COUNTRY_TLD.put("CH", "ch"); // SWITZERLAND + GOOGLE_PRODUCT_SEARCH_COUNTRY_TLD.put(Locale.UK.getCountry(), "co.uk"); + GOOGLE_PRODUCT_SEARCH_COUNTRY_TLD.put(Locale.US.getCountry(), "com"); + } + + /** + * Book search is offered everywhere that web search is available. + */ + private static final Map GOOGLE_BOOK_SEARCH_COUNTRY_TLD = GOOGLE_COUNTRY_TLD; + + private static final Collection TRANSLATED_HELP_ASSET_LANGUAGES = + Arrays.asList("de", "en", "es", "fr", "it", "ja", "ko", "nl", "pt", "ru", "uk", "zh-rCN", "zh-rTW", "zh-rHK"); + + private LocaleManager() {} + + /** + * @param context application's {@link Context} + * @return country-specific TLD suffix appropriate for the current default locale + * (e.g. "co.uk" for the United Kingdom) + */ + public static String getCountryTLD(Context context) { + return doGetTLD(GOOGLE_COUNTRY_TLD, context); + } + + /** + * The same as above, but specifically for Google Product Search. + * + * @param context application's {@link Context} + * @return The top-level domain to use. + */ + public static String getProductSearchCountryTLD(Context context) { + return doGetTLD(GOOGLE_PRODUCT_SEARCH_COUNTRY_TLD, context); + } + + /** + * The same as above, but specifically for Google Book Search. + * + * @param context application's {@link Context} + * @return The top-level domain to use. + */ + public static String getBookSearchCountryTLD(Context context) { + return doGetTLD(GOOGLE_BOOK_SEARCH_COUNTRY_TLD, context); + } + + /** + * Does a given URL point to Google Book Search, regardless of domain. + * + * @param url The address to check. + * @return True if this is a Book Search URL. + */ + public static boolean isBookSearchUrl(String url) { + return url.startsWith("http://google.com/books") || url.startsWith("http://books.google."); + } + + private static String getSystemCountry() { + Locale locale = Locale.getDefault(); + return locale == null ? DEFAULT_COUNTRY : locale.getCountry(); + } + + private static String getSystemLanguage() { + Locale locale = Locale.getDefault(); + if (locale == null) { + return DEFAULT_LANGUAGE; + } + String language = locale.getLanguage(); + // Special case Chinese + if (Locale.SIMPLIFIED_CHINESE.getLanguage().equals(language)) { + return language + "-r" + getSystemCountry(); + } + return language; + } + + public static String getTranslatedAssetLanguage() { + String language = getSystemLanguage(); + return TRANSLATED_HELP_ASSET_LANGUAGES.contains(language) ? language : DEFAULT_LANGUAGE; + } + + private static String doGetTLD(Map map, Context context) { + String tld = map.get(getCountry(context)); + return tld == null ? DEFAULT_TLD : tld; + } + + public static String getCountry(Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + String countryOverride = prefs.getString(PreferencesActivity.KEY_SEARCH_COUNTRY, "-"); + if (countryOverride != null && !countryOverride.isEmpty() && !"-".equals(countryOverride)) { + return countryOverride; + } + return getSystemCountry(); + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/PreferencesActivity.java b/app/src/main/java/com/google/zxing/client/android/PreferencesActivity.java new file mode 100644 index 0000000..8d0dc64 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/PreferencesActivity.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android; + +import android.app.Activity; +import android.os.Bundle; + +/** + * The main settings activity. + * + * @author dswitkin@google.com (Daniel Switkin) + * @author Sean Owen + */ +public final class PreferencesActivity extends Activity { + + public static final String KEY_DECODE_1D_PRODUCT = "preferences_decode_1D_product"; + public static final String KEY_DECODE_1D_INDUSTRIAL = "preferences_decode_1D_industrial"; + public static final String KEY_DECODE_QR = "preferences_decode_QR"; + public static final String KEY_DECODE_DATA_MATRIX = "preferences_decode_Data_Matrix"; + public static final String KEY_DECODE_AZTEC = "preferences_decode_Aztec"; + public static final String KEY_DECODE_PDF417 = "preferences_decode_PDF417"; + + public static final String KEY_CUSTOM_PRODUCT_SEARCH = "preferences_custom_product_search"; + + public static final String KEY_PLAY_BEEP = "preferences_play_beep"; + public static final String KEY_VIBRATE = "preferences_vibrate"; + public static final String KEY_COPY_TO_CLIPBOARD = "preferences_copy_to_clipboard"; + public static final String KEY_FRONT_LIGHT_MODE = "preferences_front_light_mode"; + public static final String KEY_BULK_MODE = "preferences_bulk_mode"; + public static final String KEY_REMEMBER_DUPLICATES = "preferences_remember_duplicates"; + public static final String KEY_ENABLE_HISTORY = "preferences_history"; + public static final String KEY_SUPPLEMENTAL = "preferences_supplemental"; + public static final String KEY_AUTO_FOCUS = "preferences_auto_focus"; + public static final String KEY_INVERT_SCAN = "preferences_invert_scan"; + public static final String KEY_SEARCH_COUNTRY = "preferences_search_country"; + public static final String KEY_DISABLE_AUTO_ORIENTATION = "preferences_orientation"; + + public static final String KEY_DISABLE_CONTINUOUS_FOCUS = "preferences_disable_continuous_focus"; + public static final String KEY_DISABLE_EXPOSURE = "preferences_disable_exposure"; + public static final String KEY_DISABLE_METERING = "preferences_disable_metering"; + public static final String KEY_DISABLE_BARCODE_SCENE_MODE = "preferences_disable_barcode_scene_mode"; + public static final String KEY_AUTO_OPEN_WEB = "preferences_auto_open_web"; + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + getFragmentManager().beginTransaction().replace(android.R.id.content, new PreferencesFragment()).commit(); + } + + // Apparently this will be necessary when targeting API 19+: + /* + @Override + protected boolean isValidFragment(String fragmentName) { + return true; + } + */ + +} diff --git a/app/src/main/java/com/google/zxing/client/android/PreferencesFragment.java b/app/src/main/java/com/google/zxing/client/android/PreferencesFragment.java new file mode 100644 index 0000000..6d90ef7 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/PreferencesFragment.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2013 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android; + +import android.app.AlertDialog; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.CheckBoxPreference; +import android.preference.EditTextPreference; +import android.preference.Preference; +import android.preference.PreferenceFragment; +import android.preference.PreferenceScreen; + +import net.foucry.pilldroid.R; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collection; + +public final class PreferencesFragment + extends PreferenceFragment + implements SharedPreferences.OnSharedPreferenceChangeListener { + + private CheckBoxPreference[] checkBoxPrefs; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + addPreferencesFromResource(R.xml.preferences); + + PreferenceScreen preferences = getPreferenceScreen(); + preferences.getSharedPreferences().registerOnSharedPreferenceChangeListener(this); + checkBoxPrefs = findDecodePrefs(preferences, + PreferencesActivity.KEY_DECODE_1D_PRODUCT, + PreferencesActivity.KEY_DECODE_1D_INDUSTRIAL, + PreferencesActivity.KEY_DECODE_QR, + PreferencesActivity.KEY_DECODE_DATA_MATRIX, + PreferencesActivity.KEY_DECODE_AZTEC, + PreferencesActivity.KEY_DECODE_PDF417); + disableLastCheckedPref(); + + EditTextPreference customProductSearch = (EditTextPreference) + preferences.findPreference(PreferencesActivity.KEY_CUSTOM_PRODUCT_SEARCH); + customProductSearch.setOnPreferenceChangeListener(new CustomSearchURLValidator()); + } + + private static CheckBoxPreference[] findDecodePrefs(PreferenceScreen preferences, String... keys) { + CheckBoxPreference[] prefs = new CheckBoxPreference[keys.length]; + for (int i = 0; i < keys.length; i++) { + prefs[i] = (CheckBoxPreference) preferences.findPreference(keys[i]); + } + return prefs; + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + disableLastCheckedPref(); + } + + private void disableLastCheckedPref() { + Collection checked = new ArrayList<>(checkBoxPrefs.length); + for (CheckBoxPreference pref : checkBoxPrefs) { + if (pref.isChecked()) { + checked.add(pref); + } + } + boolean disable = checked.size() <= 1; + for (CheckBoxPreference pref : checkBoxPrefs) { + pref.setEnabled(!(disable && checked.contains(pref))); + } + } + + private class CustomSearchURLValidator implements Preference.OnPreferenceChangeListener { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (!isValid(newValue)) { + AlertDialog.Builder builder = + new AlertDialog.Builder(PreferencesFragment.this.getActivity()); + builder.setTitle(R.string.msg_error); + builder.setMessage(R.string.msg_invalid_value); + builder.setCancelable(true); + builder.show(); + return false; + } + return true; + } + + private boolean isValid(Object newValue) { + // Allow empty/null value + if (newValue == null) { + return true; + } + String valueString = newValue.toString(); + if (valueString.isEmpty()) { + return true; + } + // Before validating, remove custom placeholders, which will not + // be considered valid parts of the URL in some locations: + // Blank %t and %s: + valueString = valueString.replaceAll("%[st]", ""); + // Blank %f but not if followed by digit or a-f as it may be a hex sequence + valueString = valueString.replaceAll("%f(?![0-9a-f])", ""); + // Require a scheme otherwise: + try { + URI uri = new URI(valueString); + return uri.getScheme() != null; + } catch (URISyntaxException use) { + return false; + } + } + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/ScanFromWebPageManager.java b/app/src/main/java/com/google/zxing/client/android/ScanFromWebPageManager.java new file mode 100644 index 0000000..15170b0 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/ScanFromWebPageManager.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2012 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android; + +import android.net.Uri; +import com.google.zxing.Result; +import com.google.zxing.client.android.result.ResultHandler; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +/** + * Manages functionality related to responding to requests to scan from an HTTP link in a web page. + * See ScanningFromWebPages. + * + * @author Sean Owen + */ +final class ScanFromWebPageManager { + + private static final CharSequence CODE_PLACEHOLDER = "{CODE}"; + private static final CharSequence RAW_CODE_PLACEHOLDER = "{RAWCODE}"; + private static final CharSequence META_PLACEHOLDER = "{META}"; + private static final CharSequence FORMAT_PLACEHOLDER = "{FORMAT}"; + private static final CharSequence TYPE_PLACEHOLDER = "{TYPE}"; + + private static final String RETURN_URL_PARAM = "ret"; + private static final String RAW_PARAM = "raw"; + + private final String returnUrlTemplate; + private final boolean returnRaw; + + ScanFromWebPageManager(Uri inputUri) { + returnUrlTemplate = inputUri.getQueryParameter(RETURN_URL_PARAM); + returnRaw = inputUri.getQueryParameter(RAW_PARAM) != null; + } + + boolean isScanFromWebPage() { + return returnUrlTemplate != null; + } + + String buildReplyURL(Result rawResult, ResultHandler resultHandler) { + String result = returnUrlTemplate; + result = replace(CODE_PLACEHOLDER, + returnRaw ? rawResult.getText() : resultHandler.getDisplayContents(), result); + result = replace(RAW_CODE_PLACEHOLDER, rawResult.getText(), result); + result = replace(FORMAT_PLACEHOLDER, rawResult.getBarcodeFormat().toString(), result); + result = replace(TYPE_PLACEHOLDER, resultHandler.getType().toString(), result); + result = replace(META_PLACEHOLDER, String.valueOf(rawResult.getResultMetadata()), result); + return result; + } + + private static String replace(CharSequence placeholder, CharSequence with, String pattern) { + CharSequence escapedWith = with == null ? "" : with; + try { + escapedWith = URLEncoder.encode(escapedWith.toString(), "UTF-8"); + } catch (UnsupportedEncodingException e) { + // can't happen; UTF-8 is always supported. Continue, I guess, without encoding + } + return pattern.replace(placeholder, escapedWith); + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/ViewfinderResultPointCallback.java b/app/src/main/java/com/google/zxing/client/android/ViewfinderResultPointCallback.java new file mode 100644 index 0000000..48009a9 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/ViewfinderResultPointCallback.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android; + +import com.google.zxing.ResultPoint; +import com.google.zxing.ResultPointCallback; + +final class ViewfinderResultPointCallback implements ResultPointCallback { + + private final ViewfinderView viewfinderView; + + ViewfinderResultPointCallback(ViewfinderView viewfinderView) { + this.viewfinderView = viewfinderView; + } + + @Override + public void foundPossibleResultPoint(ResultPoint point) { + viewfinderView.addPossibleResultPoint(point); + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/ViewfinderView.java b/app/src/main/java/com/google/zxing/client/android/ViewfinderView.java new file mode 100644 index 0000000..4aa499d --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/ViewfinderView.java @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.View; + +import com.google.zxing.ResultPoint; +import com.google.zxing.client.android.camera.CameraManager; + +import net.foucry.pilldroid.R; + +import java.util.ArrayList; +import java.util.List; + +/** + * This view is overlaid on top of the camera preview. It adds the viewfinder rectangle and partial + * transparency outside it, as well as the laser scanner animation and result points. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class ViewfinderView extends View { + + private static final int[] SCANNER_ALPHA = {0, 64, 128, 192, 255, 192, 128, 64}; + private static final long ANIMATION_DELAY = 80L; + private static final int CURRENT_POINT_OPACITY = 0xA0; + private static final int MAX_RESULT_POINTS = 20; + private static final int POINT_SIZE = 6; + + private CameraManager cameraManager; + private final Paint paint; + private Bitmap resultBitmap; + private final int maskColor; + private final int resultColor; + private final int laserColor; + private final int resultPointColor; + private int scannerAlpha; + private List possibleResultPoints; + private List lastPossibleResultPoints; + + // This constructor is used when the class is built from an XML resource. + public ViewfinderView(Context context, AttributeSet attrs) { + super(context, attrs); + + // Initialize these once for performance rather than calling them every time in onDraw(). + paint = new Paint(Paint.ANTI_ALIAS_FLAG); + Resources resources = getResources(); + maskColor = resources.getColor(R.color.viewfinder_mask); + resultColor = resources.getColor(R.color.result_view); + laserColor = resources.getColor(R.color.viewfinder_laser); + resultPointColor = resources.getColor(R.color.possible_result_points); + scannerAlpha = 0; + possibleResultPoints = new ArrayList<>(5); + lastPossibleResultPoints = null; + } + + public void setCameraManager(CameraManager cameraManager) { + this.cameraManager = cameraManager; + } + + @SuppressLint("DrawAllocation") + @Override + public void onDraw(Canvas canvas) { + if (cameraManager == null) { + return; // not ready yet, early draw before done configuring + } + Rect frame = cameraManager.getFramingRect(); + Rect previewFrame = cameraManager.getFramingRectInPreview(); + if (frame == null || previewFrame == null) { + return; + } + int width = canvas.getWidth(); + int height = canvas.getHeight(); + + // Draw the exterior (i.e. outside the framing rect) darkened + paint.setColor(resultBitmap != null ? resultColor : maskColor); + canvas.drawRect(0, 0, width, frame.top, paint); + canvas.drawRect(0, frame.top, frame.left, frame.bottom + 1, paint); + canvas.drawRect(frame.right + 1, frame.top, width, frame.bottom + 1, paint); + canvas.drawRect(0, frame.bottom + 1, width, height, paint); + + if (resultBitmap != null) { + // Draw the opaque result bitmap over the scanning rectangle + paint.setAlpha(CURRENT_POINT_OPACITY); + canvas.drawBitmap(resultBitmap, null, frame, paint); + } else { + + // Draw a red "laser scanner" line through the middle to show decoding is active + paint.setColor(laserColor); + paint.setAlpha(SCANNER_ALPHA[scannerAlpha]); + scannerAlpha = (scannerAlpha + 1) % SCANNER_ALPHA.length; + int middle = frame.height() / 2 + frame.top; + canvas.drawRect(frame.left + 2, middle - 1, frame.right - 1, middle + 2, paint); + + float scaleX = frame.width() / (float) previewFrame.width(); + float scaleY = frame.height() / (float) previewFrame.height(); + + List currentPossible = possibleResultPoints; + List currentLast = lastPossibleResultPoints; + int frameLeft = frame.left; + int frameTop = frame.top; + if (currentPossible.isEmpty()) { + lastPossibleResultPoints = null; + } else { + possibleResultPoints = new ArrayList<>(5); + lastPossibleResultPoints = currentPossible; + paint.setAlpha(CURRENT_POINT_OPACITY); + paint.setColor(resultPointColor); + synchronized (currentPossible) { + for (ResultPoint point : currentPossible) { + canvas.drawCircle(frameLeft + (int) (point.getX() * scaleX), + frameTop + (int) (point.getY() * scaleY), + POINT_SIZE, paint); + } + } + } + if (currentLast != null) { + paint.setAlpha(CURRENT_POINT_OPACITY / 2); + paint.setColor(resultPointColor); + synchronized (currentLast) { + float radius = POINT_SIZE / 2.0f; + for (ResultPoint point : currentLast) { + canvas.drawCircle(frameLeft + (int) (point.getX() * scaleX), + frameTop + (int) (point.getY() * scaleY), + radius, paint); + } + } + } + + // Request another update at the animation interval, but only repaint the laser line, + // not the entire viewfinder mask. + postInvalidateDelayed(ANIMATION_DELAY, + frame.left - POINT_SIZE, + frame.top - POINT_SIZE, + frame.right + POINT_SIZE, + frame.bottom + POINT_SIZE); + } + } + + public void drawViewfinder() { + Bitmap resultBitmap = this.resultBitmap; + this.resultBitmap = null; + if (resultBitmap != null) { + resultBitmap.recycle(); + } + invalidate(); + } + + /** + * Draw a bitmap with the result points highlighted instead of the live scanning display. + * + * @param barcode An image of the decoded barcode. + */ + public void drawResultBitmap(Bitmap barcode) { + resultBitmap = barcode; + invalidate(); + } + + public void addPossibleResultPoint(ResultPoint point) { + List points = possibleResultPoints; + synchronized (points) { + points.add(point); + int size = points.size(); + if (size > MAX_RESULT_POINTS) { + // trim it + points.subList(0, size - MAX_RESULT_POINTS / 2).clear(); + } + } + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/book/BrowseBookListener.java b/app/src/main/java/com/google/zxing/client/android/book/BrowseBookListener.java new file mode 100644 index 0000000..c00bd6e --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/book/BrowseBookListener.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.book; + +import android.content.Intent; +import android.net.Uri; +import android.view.View; +import android.widget.AdapterView; +import com.google.zxing.client.android.LocaleManager; + +import java.util.List; + +final class BrowseBookListener implements AdapterView.OnItemClickListener { + + private final SearchBookContentsActivity activity; + private final List items; + + BrowseBookListener(SearchBookContentsActivity activity, List items) { + this.activity = activity; + this.items = items; + } + + @Override + public void onItemClick(AdapterView parent, View v, int position, long id) { + if (position < 1) { + // Clicked header, ignore it + return; + } + int itemOffset = position - 1; + if (itemOffset >= items.size()) { + return; + } + String pageId = items.get(itemOffset).getPageId(); + String query = SearchBookContentsResult.getQuery(); + if (LocaleManager.isBookSearchUrl(activity.getISBN()) && !pageId.isEmpty()) { + String uri = activity.getISBN(); + int equals = uri.indexOf('='); + String volumeId = uri.substring(equals + 1); + String readBookURI = "http://books.google." + + LocaleManager.getBookSearchCountryTLD(activity) + + "/books?id=" + volumeId + "&pg=" + pageId + "&vq=" + query; + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(readBookURI)); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + activity.startActivity(intent); + } + } +} diff --git a/app/src/main/java/com/google/zxing/client/android/book/SearchBookContentsActivity.java b/app/src/main/java/com/google/zxing/client/android/book/SearchBookContentsActivity.java new file mode 100644 index 0000000..9c14cfb --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/book/SearchBookContentsActivity.java @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.book; + +import android.app.Activity; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Bundle; +import android.util.Log; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.webkit.CookieManager; +import android.webkit.CookieSyncManager; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.TextView; + +import com.google.zxing.client.android.HttpHelper; +import com.google.zxing.client.android.Intents; +import com.google.zxing.client.android.LocaleManager; + +import net.foucry.pilldroid.R; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; +/** + * Uses Google Book Search to find a word or phrase in the requested book. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class SearchBookContentsActivity extends Activity { + + private static final String TAG = SearchBookContentsActivity.class.getSimpleName(); + + private static final Pattern TAG_PATTERN = Pattern.compile("\\<.*?\\>"); + private static final Pattern LT_ENTITY_PATTERN = Pattern.compile("<"); + private static final Pattern GT_ENTITY_PATTERN = Pattern.compile(">"); + private static final Pattern QUOTE_ENTITY_PATTERN = Pattern.compile("'"); + private static final Pattern QUOT_ENTITY_PATTERN = Pattern.compile("""); + + private String isbn; + private EditText queryTextView; + private View queryButton; + private ListView resultListView; + private TextView headerView; + private AsyncTask networkTask; + + private final View.OnClickListener buttonListener = new View.OnClickListener() { + @Override + public void onClick(View view) { + launchSearch(); + } + }; + + private final View.OnKeyListener keyListener = new View.OnKeyListener() { + @Override + public boolean onKey(View view, int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_DOWN) { + launchSearch(); + return true; + } + return false; + } + }; + + String getISBN() { + return isbn; + } + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + // Make sure that expired cookies are removed on launch. + CookieSyncManager.createInstance(this); + CookieManager.getInstance().removeExpiredCookie(); + + Intent intent = getIntent(); + if (intent == null || !Intents.SearchBookContents.ACTION.equals(intent.getAction())) { + finish(); + return; + } + + isbn = intent.getStringExtra(Intents.SearchBookContents.ISBN); + if (LocaleManager.isBookSearchUrl(isbn)) { + setTitle(getString(R.string.sbc_name)); + } else { + setTitle(getString(R.string.sbc_name) + ": ISBN " + isbn); + } + + setContentView(R.layout.search_book_contents); + queryTextView = (EditText) findViewById(R.id.query_text_view); + + String initialQuery = intent.getStringExtra(Intents.SearchBookContents.QUERY); + if (initialQuery != null && !initialQuery.isEmpty()) { + // Populate the search box but don't trigger the search + queryTextView.setText(initialQuery); + } + queryTextView.setOnKeyListener(keyListener); + + queryButton = findViewById(R.id.query_button); + queryButton.setOnClickListener(buttonListener); + + resultListView = (ListView) findViewById(R.id.result_list_view); + LayoutInflater factory = LayoutInflater.from(this); + headerView = (TextView) factory.inflate(R.layout.search_book_contents_header, + resultListView, false); + resultListView.addHeaderView(headerView); + } + + @Override + protected void onResume() { + super.onResume(); + queryTextView.selectAll(); + } + + @Override + protected void onPause() { + AsyncTask oldTask = networkTask; + if (oldTask != null) { + oldTask.cancel(true); + networkTask = null; + } + super.onPause(); + } + + private void launchSearch() { + String query = queryTextView.getText().toString(); + if (query != null && !query.isEmpty()) { + AsyncTask oldTask = networkTask; + if (oldTask != null) { + oldTask.cancel(true); + } + networkTask = new NetworkTask(); + networkTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, query, isbn); + headerView.setText(R.string.msg_sbc_searching_book); + resultListView.setAdapter(null); + queryTextView.setEnabled(false); + queryButton.setEnabled(false); + } + } + + private final class NetworkTask extends AsyncTask { + + @Override + protected JSONObject doInBackground(String... args) { + try { + // These return a JSON result which describes if and where the query was found. This API may + // break or disappear at any time in the future. Since this is an API call rather than a + // website, we don't use LocaleManager to change the TLD. + String theQuery = args[0]; + String theIsbn = args[1]; + String uri; + if (LocaleManager.isBookSearchUrl(theIsbn)) { + int equals = theIsbn.indexOf('='); + String volumeId = theIsbn.substring(equals + 1); + uri = "http://www.google.com/books?id=" + volumeId + "&jscmd=SearchWithinVolume2&q=" + theQuery; + } else { + uri = "http://www.google.com/books?vid=isbn" + theIsbn + "&jscmd=SearchWithinVolume2&q=" + theQuery; + } + CharSequence content = HttpHelper.downloadViaHttp(uri, HttpHelper.ContentType.JSON); + return new JSONObject(content.toString()); + } catch (IOException ioe) { + Log.w(TAG, "Error accessing book search", ioe); + return null; + } catch (JSONException je) { + Log.w(TAG, "Error accessing book search", je); + return null; + } + } + + @Override + protected void onPostExecute(JSONObject result) { + if (result == null) { + headerView.setText(R.string.msg_sbc_failed); + } else { + handleSearchResults(result); + } + queryTextView.setEnabled(true); + queryTextView.selectAll(); + queryButton.setEnabled(true); + } + + // Currently there is no way to distinguish between a query which had no results and a book + // which is not searchable - both return zero results. + private void handleSearchResults(JSONObject json) { + try { + int count = json.getInt("number_of_results"); + headerView.setText(getString(R.string.msg_sbc_results) + " : " + count); + if (count > 0) { + JSONArray results = json.getJSONArray("search_results"); + SearchBookContentsResult.setQuery(queryTextView.getText().toString()); + List items = new ArrayList<>(count); + for (int x = 0; x < count; x++) { + items.add(parseResult(results.getJSONObject(x))); + } + resultListView.setOnItemClickListener(new BrowseBookListener(SearchBookContentsActivity.this, items)); + resultListView.setAdapter(new SearchBookContentsAdapter(SearchBookContentsActivity.this, items)); + } else { + String searchable = json.optString("searchable"); + if ("false".equals(searchable)) { + headerView.setText(R.string.msg_sbc_book_not_searchable); + } + resultListView.setAdapter(null); + } + } catch (JSONException e) { + Log.w(TAG, "Bad JSON from book search", e); + resultListView.setAdapter(null); + headerView.setText(R.string.msg_sbc_failed); + } + } + + // Available fields: page_id, page_number, snippet_text + private SearchBookContentsResult parseResult(JSONObject json) { + + String pageId; + String pageNumber; + String snippet; + try { + pageId = json.getString("page_id"); + pageNumber = json.optString("page_number"); + snippet = json.optString("snippet_text"); + } catch (JSONException e) { + Log.w(TAG, e); + // Never seen in the wild, just being complete. + return new SearchBookContentsResult(getString(R.string.msg_sbc_no_page_returned), "", "", false); + } + + if (pageNumber == null || pageNumber.isEmpty()) { + // This can happen for text on the jacket, and possibly other reasons. + pageNumber = ""; + } else { + pageNumber = getString(R.string.msg_sbc_page) + ' ' + pageNumber; + } + + boolean valid = snippet != null && !snippet.isEmpty(); + if (valid) { + // Remove all HTML tags and encoded characters. + snippet = TAG_PATTERN.matcher(snippet).replaceAll(""); + snippet = LT_ENTITY_PATTERN.matcher(snippet).replaceAll("<"); + snippet = GT_ENTITY_PATTERN.matcher(snippet).replaceAll(">"); + snippet = QUOTE_ENTITY_PATTERN.matcher(snippet).replaceAll("'"); + snippet = QUOT_ENTITY_PATTERN.matcher(snippet).replaceAll("\""); + } else { + snippet = '(' + getString(R.string.msg_sbc_snippet_unavailable) + ')'; + } + + return new SearchBookContentsResult(pageId, pageNumber, snippet, valid); + } + + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/book/SearchBookContentsAdapter.java b/app/src/main/java/com/google/zxing/client/android/book/SearchBookContentsAdapter.java new file mode 100644 index 0000000..84f5889 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/book/SearchBookContentsAdapter.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.book; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; + +import net.foucry.pilldroid.R; + +import java.util.List; +/** + * Manufactures list items which represent SBC results. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +final class SearchBookContentsAdapter extends ArrayAdapter { + + SearchBookContentsAdapter(Context context, List items) { + super(context, R.layout.search_book_contents_list_item, 0, items); + } + + @Override + public View getView(int position, View view, ViewGroup viewGroup) { + SearchBookContentsListItem listItem; + + if (view == null) { + LayoutInflater factory = LayoutInflater.from(getContext()); + listItem = (SearchBookContentsListItem) factory.inflate( + R.layout.search_book_contents_list_item, viewGroup, false); + } else { + if (view instanceof SearchBookContentsListItem) { + listItem = (SearchBookContentsListItem) view; + } else { + return view; + } + } + + SearchBookContentsResult result = getItem(position); + listItem.set(result); + return listItem; + } +} diff --git a/app/src/main/java/com/google/zxing/client/android/book/SearchBookContentsListItem.java b/app/src/main/java/com/google/zxing/client/android/book/SearchBookContentsListItem.java new file mode 100644 index 0000000..5ef5d58 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/book/SearchBookContentsListItem.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.book; + +import android.content.Context; +import android.graphics.Typeface; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.StyleSpan; +import android.util.AttributeSet; +import android.widget.LinearLayout; +import android.widget.TextView; + +import net.foucry.pilldroid.R; + +import java.util.Locale; + +/** + * A list item which displays the page number and snippet of this search result. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class SearchBookContentsListItem extends LinearLayout { + private TextView pageNumberView; + private TextView snippetView; + + SearchBookContentsListItem(Context context) { + super(context); + } + + public SearchBookContentsListItem(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + pageNumberView = (TextView) findViewById(R.id.page_number_view); + snippetView = (TextView) findViewById(R.id.snippet_view); + } + + public void set(SearchBookContentsResult result) { + pageNumberView.setText(result.getPageNumber()); + String snippet = result.getSnippet(); + if (snippet.isEmpty()) { + snippetView.setText(""); + } else { + if (result.getValidSnippet()) { + String lowerQuery = SearchBookContentsResult.getQuery().toLowerCase(Locale.getDefault()); + String lowerSnippet = snippet.toLowerCase(Locale.getDefault()); + Spannable styledSnippet = new SpannableString(snippet); + StyleSpan boldSpan = new StyleSpan(Typeface.BOLD); + int queryLength = lowerQuery.length(); + int offset = 0; + while (true) { + int pos = lowerSnippet.indexOf(lowerQuery, offset); + if (pos < 0) { + break; + } + styledSnippet.setSpan(boldSpan, pos, pos + queryLength, 0); + offset = pos + queryLength; + } + snippetView.setText(styledSnippet); + } else { + // This may be an error message, so don't try to bold the query terms within it + snippetView.setText(snippet); + } + } + } +} diff --git a/app/src/main/java/com/google/zxing/client/android/book/SearchBookContentsResult.java b/app/src/main/java/com/google/zxing/client/android/book/SearchBookContentsResult.java new file mode 100644 index 0000000..ffaafb3 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/book/SearchBookContentsResult.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.book; + +/** + * The underlying data for a SBC result. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +final class SearchBookContentsResult { + + private static String query = null; + + private final String pageId; + private final String pageNumber; + private final String snippet; + private final boolean validSnippet; + + SearchBookContentsResult(String pageId, + String pageNumber, + String snippet, + boolean validSnippet) { + this.pageId = pageId; + this.pageNumber = pageNumber; + this.snippet = snippet; + this.validSnippet = validSnippet; + } + + public static void setQuery(String query) { + SearchBookContentsResult.query = query; + } + + public String getPageId() { + return pageId; + } + + public String getPageNumber() { + return pageNumber; + } + + public String getSnippet() { + return snippet; + } + + public boolean getValidSnippet() { + return validSnippet; + } + + public static String getQuery() { + return query; + } +} diff --git a/app/src/main/java/com/google/zxing/client/android/camera/AutoFocusManager.java b/app/src/main/java/com/google/zxing/client/android/camera/AutoFocusManager.java new file mode 100644 index 0000000..05aee0a --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/camera/AutoFocusManager.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2012 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.camera; + +import android.content.Context; +import android.content.SharedPreferences; +import android.hardware.Camera; +import android.os.AsyncTask; +import android.preference.PreferenceManager; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.RejectedExecutionException; + +import com.google.zxing.client.android.PreferencesActivity; + +final class AutoFocusManager implements Camera.AutoFocusCallback { + + private static final String TAG = AutoFocusManager.class.getSimpleName(); + + private static final long AUTO_FOCUS_INTERVAL_MS = 2000L; + private static final Collection FOCUS_MODES_CALLING_AF; + static { + FOCUS_MODES_CALLING_AF = new ArrayList<>(2); + FOCUS_MODES_CALLING_AF.add(Camera.Parameters.FOCUS_MODE_AUTO); + FOCUS_MODES_CALLING_AF.add(Camera.Parameters.FOCUS_MODE_MACRO); + } + + private boolean stopped; + private boolean focusing; + private final boolean useAutoFocus; + private final Camera camera; + private AsyncTask outstandingTask; + + AutoFocusManager(Context context, Camera camera) { + this.camera = camera; + SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); + String currentFocusMode = camera.getParameters().getFocusMode(); + useAutoFocus = + sharedPrefs.getBoolean(PreferencesActivity.KEY_AUTO_FOCUS, true) && + FOCUS_MODES_CALLING_AF.contains(currentFocusMode); + Log.i(TAG, "Current focus mode '" + currentFocusMode + "'; use auto focus? " + useAutoFocus); + start(); + } + + @Override + public synchronized void onAutoFocus(boolean success, Camera theCamera) { + focusing = false; + autoFocusAgainLater(); + } + + private synchronized void autoFocusAgainLater() { + if (!stopped && outstandingTask == null) { + AutoFocusTask newTask = new AutoFocusTask(); + try { + newTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + outstandingTask = newTask; + } catch (RejectedExecutionException ree) { + Log.w(TAG, "Could not request auto focus", ree); + } + } + } + + synchronized void start() { + if (useAutoFocus) { + outstandingTask = null; + if (!stopped && !focusing) { + try { + camera.autoFocus(this); + focusing = true; + } catch (RuntimeException re) { + // Have heard RuntimeException reported in Android 4.0.x+; continue? + Log.w(TAG, "Unexpected exception while focusing", re); + // Try again later to keep cycle going + autoFocusAgainLater(); + } + } + } + } + + private synchronized void cancelOutstandingTask() { + if (outstandingTask != null) { + if (outstandingTask.getStatus() != AsyncTask.Status.FINISHED) { + outstandingTask.cancel(true); + } + outstandingTask = null; + } + } + + synchronized void stop() { + stopped = true; + if (useAutoFocus) { + cancelOutstandingTask(); + // Doesn't hurt to call this even if not focusing + try { + camera.cancelAutoFocus(); + } catch (RuntimeException re) { + // Have heard RuntimeException reported in Android 4.0.x+; continue? + Log.w(TAG, "Unexpected exception while cancelling focusing", re); + } + } + } + + private final class AutoFocusTask extends AsyncTask { + @Override + protected Object doInBackground(Object... voids) { + try { + Thread.sleep(AUTO_FOCUS_INTERVAL_MS); + } catch (InterruptedException e) { + // continue + } + start(); + return null; + } + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/camera/CameraConfigurationManager.java b/app/src/main/java/com/google/zxing/client/android/camera/CameraConfigurationManager.java new file mode 100644 index 0000000..a18e37e --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/camera/CameraConfigurationManager.java @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.camera; + +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.Point; +import android.hardware.Camera; +import android.preference.PreferenceManager; +import android.util.Log; +import android.view.Display; +import android.view.Surface; +import android.view.WindowManager; + +import com.google.zxing.client.android.PreferencesActivity; +import com.google.zxing.client.android.camera.open.CameraFacing; +import com.google.zxing.client.android.camera.open.OpenCamera; + +/** + * A class which deals with reading, parsing, and setting the camera parameters which are used to + * configure the camera hardware. + */ +final class CameraConfigurationManager { + + private static final String TAG = "CameraConfiguration"; + + private final Context context; + private int cwNeededRotation; + private int cwRotationFromDisplayToCamera; + private Point screenResolution; + private Point cameraResolution; + private Point bestPreviewSize; + private Point previewSizeOnScreen; + + CameraConfigurationManager(Context context) { + this.context = context; + } + + /** + * Reads, one time, values from the camera that are needed by the app. + */ + void initFromCameraParameters(OpenCamera camera) { + Camera.Parameters parameters = camera.getCamera().getParameters(); + WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + Display display = manager.getDefaultDisplay(); + + int displayRotation = display.getRotation(); + int cwRotationFromNaturalToDisplay; + switch (displayRotation) { + case Surface.ROTATION_0: + cwRotationFromNaturalToDisplay = 0; + break; + case Surface.ROTATION_90: + cwRotationFromNaturalToDisplay = 90; + break; + case Surface.ROTATION_180: + cwRotationFromNaturalToDisplay = 180; + break; + case Surface.ROTATION_270: + cwRotationFromNaturalToDisplay = 270; + break; + default: + // Have seen this return incorrect values like -90 + if (displayRotation % 90 == 0) { + cwRotationFromNaturalToDisplay = (360 + displayRotation) % 360; + } else { + throw new IllegalArgumentException("Bad rotation: " + displayRotation); + } + } + Log.i(TAG, "Display at: " + cwRotationFromNaturalToDisplay); + + int cwRotationFromNaturalToCamera = camera.getOrientation(); + Log.i(TAG, "Camera at: " + cwRotationFromNaturalToCamera); + + // Still not 100% sure about this. But acts like we need to flip this: + if (camera.getFacing() == CameraFacing.FRONT) { + cwRotationFromNaturalToCamera = (360 - cwRotationFromNaturalToCamera) % 360; + Log.i(TAG, "Front camera overriden to: " + cwRotationFromNaturalToCamera); + } + + /* + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + String overrideRotationString; + if (camera.getFacing() == CameraFacing.FRONT) { + overrideRotationString = prefs.getString(PreferencesActivity.KEY_FORCE_CAMERA_ORIENTATION_FRONT, null); + } else { + overrideRotationString = prefs.getString(PreferencesActivity.KEY_FORCE_CAMERA_ORIENTATION, null); + } + if (overrideRotationString != null && !"-".equals(overrideRotationString)) { + Log.i(TAG, "Overriding camera manually to " + overrideRotationString); + cwRotationFromNaturalToCamera = Integer.parseInt(overrideRotationString); + } + */ + + cwRotationFromDisplayToCamera = + (360 + cwRotationFromNaturalToCamera - cwRotationFromNaturalToDisplay) % 360; + Log.i(TAG, "Final display orientation: " + cwRotationFromDisplayToCamera); + if (camera.getFacing() == CameraFacing.FRONT) { + Log.i(TAG, "Compensating rotation for front camera"); + cwNeededRotation = (360 - cwRotationFromDisplayToCamera) % 360; + } else { + cwNeededRotation = cwRotationFromDisplayToCamera; + } + Log.i(TAG, "Clockwise rotation from display to camera: " + cwNeededRotation); + + Point theScreenResolution = new Point(); + display.getSize(theScreenResolution); + screenResolution = theScreenResolution; + Log.i(TAG, "Screen resolution in current orientation: " + screenResolution); + cameraResolution = CameraConfigurationUtils.findBestPreviewSizeValue(parameters, screenResolution); + Log.i(TAG, "Camera resolution: " + cameraResolution); + bestPreviewSize = CameraConfigurationUtils.findBestPreviewSizeValue(parameters, screenResolution); + Log.i(TAG, "Best available preview size: " + bestPreviewSize); + + boolean isScreenPortrait = screenResolution.x < screenResolution.y; + boolean isPreviewSizePortrait = bestPreviewSize.x < bestPreviewSize.y; + + if (isScreenPortrait == isPreviewSizePortrait) { + previewSizeOnScreen = bestPreviewSize; + } else { + previewSizeOnScreen = new Point(bestPreviewSize.y, bestPreviewSize.x); + } + Log.i(TAG, "Preview size on screen: " + previewSizeOnScreen); + } + + void setDesiredCameraParameters(OpenCamera camera, boolean safeMode) { + + Camera theCamera = camera.getCamera(); + Camera.Parameters parameters = theCamera.getParameters(); + + if (parameters == null) { + Log.w(TAG, "Device error: no camera parameters are available. Proceeding without configuration."); + return; + } + + Log.i(TAG, "Initial camera parameters: " + parameters.flatten()); + + if (safeMode) { + Log.w(TAG, "In camera config safe mode -- most settings will not be honored"); + } + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + + initializeTorch(parameters, prefs, safeMode); + + CameraConfigurationUtils.setFocus( + parameters, + prefs.getBoolean(PreferencesActivity.KEY_AUTO_FOCUS, true), + prefs.getBoolean(PreferencesActivity.KEY_DISABLE_CONTINUOUS_FOCUS, true), + safeMode); + + if (!safeMode) { + if (prefs.getBoolean(PreferencesActivity.KEY_INVERT_SCAN, false)) { + CameraConfigurationUtils.setInvertColor(parameters); + } + + if (!prefs.getBoolean(PreferencesActivity.KEY_DISABLE_BARCODE_SCENE_MODE, true)) { + CameraConfigurationUtils.setBarcodeSceneMode(parameters); + } + + if (!prefs.getBoolean(PreferencesActivity.KEY_DISABLE_METERING, true)) { + CameraConfigurationUtils.setVideoStabilization(parameters); + CameraConfigurationUtils.setFocusArea(parameters); + CameraConfigurationUtils.setMetering(parameters); + } + + } + + parameters.setPreviewSize(bestPreviewSize.x, bestPreviewSize.y); + + theCamera.setParameters(parameters); + + theCamera.setDisplayOrientation(cwRotationFromDisplayToCamera); + + Camera.Parameters afterParameters = theCamera.getParameters(); + Camera.Size afterSize = afterParameters.getPreviewSize(); + if (afterSize != null && (bestPreviewSize.x != afterSize.width || bestPreviewSize.y != afterSize.height)) { + Log.w(TAG, "Camera said it supported preview size " + bestPreviewSize.x + 'x' + bestPreviewSize.y + + ", but after setting it, preview size is " + afterSize.width + 'x' + afterSize.height); + bestPreviewSize.x = afterSize.width; + bestPreviewSize.y = afterSize.height; + } + } + + Point getBestPreviewSize() { + return bestPreviewSize; + } + + Point getPreviewSizeOnScreen() { + return previewSizeOnScreen; + } + + Point getCameraResolution() { + return cameraResolution; + } + + Point getScreenResolution() { + return screenResolution; + } + + int getCWNeededRotation() { + return cwNeededRotation; + } + + boolean getTorchState(Camera camera) { + if (camera != null) { + Camera.Parameters parameters = camera.getParameters(); + if (parameters != null) { + String flashMode = camera.getParameters().getFlashMode(); + return flashMode != null && + (Camera.Parameters.FLASH_MODE_ON.equals(flashMode) || + Camera.Parameters.FLASH_MODE_TORCH.equals(flashMode)); + } + } + return false; + } + + void setTorch(Camera camera, boolean newSetting) { + Camera.Parameters parameters = camera.getParameters(); + doSetTorch(parameters, newSetting, false); + camera.setParameters(parameters); + } + + private void initializeTorch(Camera.Parameters parameters, SharedPreferences prefs, boolean safeMode) { + boolean currentSetting = FrontLightMode.readPref(prefs) == FrontLightMode.ON; + doSetTorch(parameters, currentSetting, safeMode); + } + + private void doSetTorch(Camera.Parameters parameters, boolean newSetting, boolean safeMode) { + CameraConfigurationUtils.setTorch(parameters, newSetting); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + if (!safeMode && !prefs.getBoolean(PreferencesActivity.KEY_DISABLE_EXPOSURE, true)) { + CameraConfigurationUtils.setBestExposure(parameters, newSetting); + } + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/camera/CameraManager.java b/app/src/main/java/com/google/zxing/client/android/camera/CameraManager.java new file mode 100644 index 0000000..9e5478f --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/camera/CameraManager.java @@ -0,0 +1,332 @@ +/* + * Copyright (C) 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.camera; + +import android.content.Context; +import android.graphics.Point; +import android.graphics.Rect; +import android.hardware.Camera; +import android.os.Handler; +import android.util.Log; +import android.view.SurfaceHolder; +import com.google.zxing.PlanarYUVLuminanceSource; +import com.google.zxing.client.android.camera.open.OpenCamera; +import com.google.zxing.client.android.camera.open.OpenCameraInterface; + +import java.io.IOException; + +/** + * This object wraps the Camera service object and expects to be the only one talking to it. The + * implementation encapsulates the steps needed to take preview-sized images, which are used for + * both preview and decoding. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class CameraManager { + + private static final String TAG = CameraManager.class.getSimpleName(); + + private static final int MIN_FRAME_WIDTH = 240; + private static final int MIN_FRAME_HEIGHT = 240; + private static final int MAX_FRAME_WIDTH = 1200; // = 5/8 * 1920 + private static final int MAX_FRAME_HEIGHT = 675; // = 5/8 * 1080 + + private final Context context; + private final CameraConfigurationManager configManager; + private OpenCamera camera; + private AutoFocusManager autoFocusManager; + private Rect framingRect; + private Rect framingRectInPreview; + private boolean initialized; + private boolean previewing; + private int requestedCameraId = OpenCameraInterface.NO_REQUESTED_CAMERA; + private int requestedFramingRectWidth; + private int requestedFramingRectHeight; + /** + * Preview frames are delivered here, which we pass on to the registered handler. Make sure to + * clear the handler so it will only receive one message. + */ + private final PreviewCallback previewCallback; + + public CameraManager(Context context) { + this.context = context; + this.configManager = new CameraConfigurationManager(context); + previewCallback = new PreviewCallback(configManager); + } + + /** + * Opens the camera driver and initializes the hardware parameters. + * + * @param holder The surface object which the camera will draw preview frames into. + * @throws IOException Indicates the camera driver failed to open. + */ + public synchronized void openDriver(SurfaceHolder holder) throws IOException { + OpenCamera theCamera = camera; + if (theCamera == null) { + theCamera = OpenCameraInterface.open(requestedCameraId); + if (theCamera == null) { + throw new IOException("Camera.open() failed to return object from driver"); + } + camera = theCamera; + } + + if (!initialized) { + initialized = true; + configManager.initFromCameraParameters(theCamera); + if (requestedFramingRectWidth > 0 && requestedFramingRectHeight > 0) { + setManualFramingRect(requestedFramingRectWidth, requestedFramingRectHeight); + requestedFramingRectWidth = 0; + requestedFramingRectHeight = 0; + } + } + + Camera cameraObject = theCamera.getCamera(); + Camera.Parameters parameters = cameraObject.getParameters(); + String parametersFlattened = parameters == null ? null : parameters.flatten(); // Save these, temporarily + try { + configManager.setDesiredCameraParameters(theCamera, false); + } catch (RuntimeException re) { + // Driver failed + Log.w(TAG, "Camera rejected parameters. Setting only minimal safe-mode parameters"); + Log.i(TAG, "Resetting to saved camera params: " + parametersFlattened); + // Reset: + if (parametersFlattened != null) { + parameters = cameraObject.getParameters(); + parameters.unflatten(parametersFlattened); + try { + cameraObject.setParameters(parameters); + configManager.setDesiredCameraParameters(theCamera, true); + } catch (RuntimeException re2) { + // Well, darn. Give up + Log.w(TAG, "Camera rejected even safe-mode parameters! No configuration"); + } + } + } + cameraObject.setPreviewDisplay(holder); + + } + + public synchronized boolean isOpen() { + return camera != null; + } + + /** + * Closes the camera driver if still in use. + */ + public synchronized void closeDriver() { + if (camera != null) { + camera.getCamera().release(); + camera = null; + // Make sure to clear these each time we close the camera, so that any scanning rect + // requested by intent is forgotten. + framingRect = null; + framingRectInPreview = null; + } + } + + /** + * Asks the camera hardware to begin drawing preview frames to the screen. + */ + public synchronized void startPreview() { + OpenCamera theCamera = camera; + if (theCamera != null && !previewing) { + theCamera.getCamera().startPreview(); + previewing = true; + autoFocusManager = new AutoFocusManager(context, theCamera.getCamera()); + } + } + + /** + * Tells the camera to stop drawing preview frames. + */ + public synchronized void stopPreview() { + if (autoFocusManager != null) { + autoFocusManager.stop(); + autoFocusManager = null; + } + if (camera != null && previewing) { + camera.getCamera().stopPreview(); + previewCallback.setHandler(null, 0); + previewing = false; + } + } + + /** + * Convenience method for {@link com.google.zxing.client.android.CaptureActivity} + * + * @param newSetting if {@code true}, light should be turned on if currently off. And vice versa. + */ + public synchronized void setTorch(boolean newSetting) { + OpenCamera theCamera = camera; + if (theCamera != null) { + if (newSetting != configManager.getTorchState(theCamera.getCamera())) { + boolean wasAutoFocusManager = autoFocusManager != null; + if (wasAutoFocusManager) { + autoFocusManager.stop(); + autoFocusManager = null; + } + configManager.setTorch(theCamera.getCamera(), newSetting); + if (wasAutoFocusManager) { + autoFocusManager = new AutoFocusManager(context, theCamera.getCamera()); + autoFocusManager.start(); + } + } + } + } + + /** + * A single preview frame will be returned to the handler supplied. The data will arrive as byte[] + * in the message.obj field, with width and height encoded as message.arg1 and message.arg2, + * respectively. + * + * @param handler The handler to send the message to. + * @param message The what field of the message to be sent. + */ + public synchronized void requestPreviewFrame(Handler handler, int message) { + OpenCamera theCamera = camera; + if (theCamera != null && previewing) { + previewCallback.setHandler(handler, message); + theCamera.getCamera().setOneShotPreviewCallback(previewCallback); + } + } + + /** + * Calculates the framing rect which the UI should draw to show the user where to place the + * barcode. This target helps with alignment as well as forces the user to hold the device + * far enough away to ensure the image will be in focus. + * + * @return The rectangle to draw on screen in window coordinates. + */ + public synchronized Rect getFramingRect() { + if (framingRect == null) { + if (camera == null) { + return null; + } + Point screenResolution = configManager.getScreenResolution(); + if (screenResolution == null) { + // Called early, before init even finished + return null; + } + + int width = findDesiredDimensionInRange(screenResolution.x, MIN_FRAME_WIDTH, MAX_FRAME_WIDTH); + int height = findDesiredDimensionInRange(screenResolution.y, MIN_FRAME_HEIGHT, MAX_FRAME_HEIGHT); + + int leftOffset = (screenResolution.x - width) / 2; + int topOffset = (screenResolution.y - height) / 2; + framingRect = new Rect(leftOffset, topOffset, leftOffset + width, topOffset + height); + Log.d(TAG, "Calculated framing rect: " + framingRect); + } + return framingRect; + } + + private static int findDesiredDimensionInRange(int resolution, int hardMin, int hardMax) { + int dim = 5 * resolution / 8; // Target 5/8 of each dimension + if (dim < hardMin) { + return hardMin; + } + if (dim > hardMax) { + return hardMax; + } + return dim; + } + + /** + * Like {@link #getFramingRect} but coordinates are in terms of the preview frame, + * not UI / screen. + * + * @return {@link Rect} expressing barcode scan area in terms of the preview size + */ + public synchronized Rect getFramingRectInPreview() { + if (framingRectInPreview == null) { + Rect framingRect = getFramingRect(); + if (framingRect == null) { + return null; + } + Rect rect = new Rect(framingRect); + Point cameraResolution = configManager.getCameraResolution(); + Point screenResolution = configManager.getScreenResolution(); + if (cameraResolution == null || screenResolution == null) { + // Called early, before init even finished + return null; + } + rect.left = rect.left * cameraResolution.x / screenResolution.x; + rect.right = rect.right * cameraResolution.x / screenResolution.x; + rect.top = rect.top * cameraResolution.y / screenResolution.y; + rect.bottom = rect.bottom * cameraResolution.y / screenResolution.y; + framingRectInPreview = rect; + } + return framingRectInPreview; + } + + + /** + * Allows third party apps to specify the camera ID, rather than determine + * it automatically based on available cameras and their orientation. + * + * @param cameraId camera ID of the camera to use. A negative value means "no preference". + */ + public synchronized void setManualCameraId(int cameraId) { + requestedCameraId = cameraId; + } + + /** + * Allows third party apps to specify the scanning rectangle dimensions, rather than determine + * them automatically based on screen resolution. + * + * @param width The width in pixels to scan. + * @param height The height in pixels to scan. + */ + public synchronized void setManualFramingRect(int width, int height) { + if (initialized) { + Point screenResolution = configManager.getScreenResolution(); + if (width > screenResolution.x) { + width = screenResolution.x; + } + if (height > screenResolution.y) { + height = screenResolution.y; + } + int leftOffset = (screenResolution.x - width) / 2; + int topOffset = (screenResolution.y - height) / 2; + framingRect = new Rect(leftOffset, topOffset, leftOffset + width, topOffset + height); + Log.d(TAG, "Calculated manual framing rect: " + framingRect); + framingRectInPreview = null; + } else { + requestedFramingRectWidth = width; + requestedFramingRectHeight = height; + } + } + + /** + * A factory method to build the appropriate LuminanceSource object based on the format + * of the preview buffers, as described by Camera.Parameters. + * + * @param data A preview frame. + * @param width The width of the image. + * @param height The height of the image. + * @return A PlanarYUVLuminanceSource instance. + */ + public PlanarYUVLuminanceSource buildLuminanceSource(byte[] data, int width, int height) { + Rect rect = getFramingRectInPreview(); + if (rect == null) { + return null; + } + // Go ahead and assume it's YUV rather than die. + return new PlanarYUVLuminanceSource(data, width, height, rect.left, rect.top, + rect.width(), rect.height(), false); + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/camera/FrontLightMode.java b/app/src/main/java/com/google/zxing/client/android/camera/FrontLightMode.java new file mode 100644 index 0000000..3fbbb59 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/camera/FrontLightMode.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2012 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.camera; + +import android.content.SharedPreferences; +import com.google.zxing.client.android.PreferencesActivity; + +/** + * Enumerates settings of the preference controlling the front light. + */ +public enum FrontLightMode { + + /** Always on. */ + ON, + /** On only when ambient light is low. */ + AUTO, + /** Always off. */ + OFF; + + private static FrontLightMode parse(String modeString) { + return modeString == null ? OFF : valueOf(modeString); + } + + public static FrontLightMode readPref(SharedPreferences sharedPrefs) { + return parse(sharedPrefs.getString(PreferencesActivity.KEY_FRONT_LIGHT_MODE, OFF.toString())); + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/camera/PreviewCallback.java b/app/src/main/java/com/google/zxing/client/android/camera/PreviewCallback.java new file mode 100644 index 0000000..7dc7035 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/camera/PreviewCallback.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.camera; + +import android.graphics.Point; +import android.hardware.Camera; +import android.os.Handler; +import android.os.Message; +import android.util.Log; + +final class PreviewCallback implements Camera.PreviewCallback { + + private static final String TAG = PreviewCallback.class.getSimpleName(); + + private final CameraConfigurationManager configManager; + private Handler previewHandler; + private int previewMessage; + + PreviewCallback(CameraConfigurationManager configManager) { + this.configManager = configManager; + } + + void setHandler(Handler previewHandler, int previewMessage) { + this.previewHandler = previewHandler; + this.previewMessage = previewMessage; + } + + @Override + public void onPreviewFrame(byte[] data, Camera camera) { + Point cameraResolution = configManager.getCameraResolution(); + Handler thePreviewHandler = previewHandler; + if (cameraResolution != null && thePreviewHandler != null) { + Message message = thePreviewHandler.obtainMessage(previewMessage, cameraResolution.x, + cameraResolution.y, data); + message.sendToTarget(); + previewHandler = null; + } else { + Log.d(TAG, "Got preview callback, but no handler or resolution available"); + } + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/camera/open/CameraFacing.java b/app/src/main/java/com/google/zxing/client/android/camera/open/CameraFacing.java new file mode 100644 index 0000000..20fd4e3 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/camera/open/CameraFacing.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2015 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.camera.open; + +public enum CameraFacing { + + BACK, // must be value 0! + FRONT, // must be value 1! + +} diff --git a/app/src/main/java/com/google/zxing/client/android/camera/open/OpenCamera.java b/app/src/main/java/com/google/zxing/client/android/camera/open/OpenCamera.java new file mode 100644 index 0000000..ddac734 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/camera/open/OpenCamera.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2015 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.camera.open; + +import android.hardware.Camera; + +public final class OpenCamera { + + private final int index; + private final Camera camera; + private final CameraFacing facing; + private final int orientation; + + public OpenCamera(int index, Camera camera, CameraFacing facing, int orientation) { + this.index = index; + this.camera = camera; + this.facing = facing; + this.orientation = orientation; + } + + public Camera getCamera() { + return camera; + } + + public CameraFacing getFacing() { + return facing; + } + + public int getOrientation() { + return orientation; + } + + @Override + public String toString() { + return "Camera #" + index + " : " + facing + ',' + orientation; + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/camera/open/OpenCameraInterface.java b/app/src/main/java/com/google/zxing/client/android/camera/open/OpenCameraInterface.java new file mode 100644 index 0000000..24e0f13 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/camera/open/OpenCameraInterface.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2012 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.camera.open; + +import android.hardware.Camera; +import android.util.Log; + +public final class OpenCameraInterface { + + private static final String TAG = OpenCameraInterface.class.getName(); + + private OpenCameraInterface() { + } + + /** For {@link #open(int)}, means no preference for which camera to open. */ + public static final int NO_REQUESTED_CAMERA = -1; + + /** + * Opens the requested camera with {@link Camera#open(int)}, if one exists. + * + * @param cameraId camera ID of the camera to use. A negative value + * or {@link #NO_REQUESTED_CAMERA} means "no preference", in which case a rear-facing + * camera is returned if possible or else any camera + * @return handle to {@link OpenCamera} that was opened + */ + public static OpenCamera open(int cameraId) { + + int numCameras = Camera.getNumberOfCameras(); + if (numCameras == 0) { + Log.w(TAG, "No cameras!"); + return null; + } + + boolean explicitRequest = cameraId >= 0; + + Camera.CameraInfo selectedCameraInfo = null; + int index; + if (explicitRequest) { + index = cameraId; + selectedCameraInfo = new Camera.CameraInfo(); + Camera.getCameraInfo(index, selectedCameraInfo); + } else { + index = 0; + while (index < numCameras) { + Camera.CameraInfo cameraInfo = new Camera.CameraInfo(); + Camera.getCameraInfo(index, cameraInfo); + CameraFacing reportedFacing = CameraFacing.values()[cameraInfo.facing]; + if (reportedFacing == CameraFacing.BACK) { + selectedCameraInfo = cameraInfo; + break; + } + index++; + } + } + + Camera camera; + if (index < numCameras) { + Log.i(TAG, "Opening camera #" + index); + camera = Camera.open(index); + } else { + if (explicitRequest) { + Log.w(TAG, "Requested camera does not exist: " + cameraId); + camera = null; + } else { + Log.i(TAG, "No camera facing " + CameraFacing.BACK + "; returning camera #0"); + camera = Camera.open(0); + selectedCameraInfo = new Camera.CameraInfo(); + Camera.getCameraInfo(0, selectedCameraInfo); + } + } + + if (camera == null) { + return null; + } + return new OpenCamera(index, + camera, + CameraFacing.values()[selectedCameraInfo.facing], + selectedCameraInfo.orientation); + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/clipboard/ClipboardInterface.java b/app/src/main/java/com/google/zxing/client/android/clipboard/ClipboardInterface.java new file mode 100644 index 0000000..3ba0603 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/clipboard/ClipboardInterface.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2012 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.clipboard; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.util.Log; + +public final class ClipboardInterface { + + private static final String TAG = ClipboardInterface.class.getSimpleName(); + + private ClipboardInterface() { + } + + public static CharSequence getText(Context context) { + ClipboardManager clipboard = getManager(context); + ClipData clip = clipboard.getPrimaryClip(); + return hasText(context) ? clip.getItemAt(0).coerceToText(context) : null; + } + + public static void setText(CharSequence text, Context context) { + if (text != null) { + try { + getManager(context).setPrimaryClip(ClipData.newPlainText(null, text)); + } catch (NullPointerException | IllegalStateException e) { + // Have seen this in the wild, bizarrely + Log.w(TAG, "Clipboard bug", e); + } + } + } + + public static boolean hasText(Context context) { + ClipboardManager clipboard = getManager(context); + ClipData clip = clipboard.getPrimaryClip(); + return clip != null && clip.getItemCount() > 0; + } + + private static ClipboardManager getManager(Context context) { + return (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/encode/ContactEncoder.java b/app/src/main/java/com/google/zxing/client/android/encode/ContactEncoder.java new file mode 100644 index 0000000..41639a9 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/encode/ContactEncoder.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2011 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.encode; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; + +/** + * Implementations encode according to some scheme for encoding contact information, like VCard or + * MECARD. + * + * @author Sean Owen + */ +abstract class ContactEncoder { + + /** + * @return first, the best effort encoding of all data in the appropriate format; second, a + * display-appropriate version of the contact information + */ + abstract String[] encode(List names, + String organization, + List addresses, + List phones, + List phoneTypes, + List emails, + List urls, + String note); + + /** + * @return null if s is null or empty, or result of s.trim() otherwise + */ + static String trim(String s) { + if (s == null) { + return null; + } + String result = s.trim(); + return result.isEmpty() ? null : result; + } + + static void append(StringBuilder newContents, + StringBuilder newDisplayContents, + String prefix, + String value, + Formatter fieldFormatter, + char terminator) { + String trimmed = trim(value); + if (trimmed != null) { + newContents.append(prefix).append(fieldFormatter.format(trimmed, 0)).append(terminator); + newDisplayContents.append(trimmed).append('\n'); + } + } + + static void appendUpToUnique(StringBuilder newContents, + StringBuilder newDisplayContents, + String prefix, + List values, + int max, + Formatter displayFormatter, + Formatter fieldFormatter, + char terminator) { + if (values == null) { + return; + } + int count = 0; + Collection uniques = new HashSet<>(2); + for (int i = 0; i < values.size(); i++) { + String value = values.get(i); + String trimmed = trim(value); + if (trimmed != null && !trimmed.isEmpty() && !uniques.contains(trimmed)) { + newContents.append(prefix).append(fieldFormatter.format(trimmed, i)).append(terminator); + CharSequence display = displayFormatter == null ? trimmed : displayFormatter.format(trimmed, i); + newDisplayContents.append(display).append('\n'); + if (++count == max) { + break; + } + uniques.add(trimmed); + } + } + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/encode/EncodeActivity.java b/app/src/main/java/com/google/zxing/client/android/encode/EncodeActivity.java new file mode 100644 index 0000000..1165c52 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/encode/EncodeActivity.java @@ -0,0 +1,243 @@ +/* + * Copyright (C) 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.encode; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Point; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.util.Log; +import android.view.Display; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.WindowManager; +import android.widget.ImageView; +import android.widget.TextView; + +import com.google.zxing.WriterException; +import com.google.zxing.client.android.Contents; +import com.google.zxing.client.android.FinishListener; +import com.google.zxing.client.android.Intents; + +import net.foucry.pilldroid.R; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.regex.Pattern; + +/** + * This class encodes data from an Intent into a QR code, and then displays it full screen so that + * another person can scan it with their device. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class EncodeActivity extends Activity { + + private static final String TAG = EncodeActivity.class.getSimpleName(); + + private static final int MAX_BARCODE_FILENAME_LENGTH = 24; + private static final Pattern NOT_ALPHANUMERIC = Pattern.compile("[^A-Za-z0-9]"); + private static final String USE_VCARD_KEY = "USE_VCARD"; + + private QRCodeEncoder qrCodeEncoder; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + Intent intent = getIntent(); + if (intent == null) { + finish(); + } else { + String action = intent.getAction(); + if (Intents.Encode.ACTION.equals(action) || Intent.ACTION_SEND.equals(action)) { + setContentView(R.layout.encode); + } else { + finish(); + } + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater menuInflater = getMenuInflater(); + menuInflater.inflate(R.menu.encode, menu); + boolean useVcard = qrCodeEncoder != null && qrCodeEncoder.isUseVCard(); + int encodeNameResource = useVcard ? R.string.menu_encode_mecard : R.string.menu_encode_vcard; + MenuItem encodeItem = menu.findItem(R.id.menu_encode); + encodeItem.setTitle(encodeNameResource); + Intent intent = getIntent(); + if (intent != null) { + String type = intent.getStringExtra(Intents.Encode.TYPE); + encodeItem.setVisible(Contents.Type.CONTACT.equals(type)); + } + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_share: + share(); + return true; + case R.id.menu_encode: + Intent intent = getIntent(); + if (intent == null) { + return false; + } + intent.putExtra(USE_VCARD_KEY, !qrCodeEncoder.isUseVCard()); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + finish(); + return true; + default: + return false; + } + } + + private void share() { + QRCodeEncoder encoder = qrCodeEncoder; + if (encoder == null) { // Odd + Log.w(TAG, "No existing barcode to send?"); + return; + } + + String contents = encoder.getContents(); + if (contents == null) { + Log.w(TAG, "No existing barcode to send?"); + return; + } + + Bitmap bitmap; + try { + bitmap = encoder.encodeAsBitmap(); + } catch (WriterException we) { + Log.w(TAG, we); + return; + } + if (bitmap == null) { + return; + } + + File bsRoot = new File(Environment.getExternalStorageDirectory(), "BarcodeScanner"); + File barcodesRoot = new File(bsRoot, "Barcodes"); + if (!barcodesRoot.exists() && !barcodesRoot.mkdirs()) { + Log.w(TAG, "Couldn't make dir " + barcodesRoot); + showErrorMessage(R.string.msg_unmount_usb); + return; + } + File barcodeFile = new File(barcodesRoot, makeBarcodeFileName(contents) + ".png"); + if (!barcodeFile.delete()) { + Log.w(TAG, "Could not delete " + barcodeFile); + // continue anyway + } + FileOutputStream fos = null; + try { + fos = new FileOutputStream(barcodeFile); + bitmap.compress(Bitmap.CompressFormat.PNG, 0, fos); + } catch (FileNotFoundException fnfe) { + Log.w(TAG, "Couldn't access file " + barcodeFile + " due to " + fnfe); + showErrorMessage(R.string.msg_unmount_usb); + return; + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException ioe) { + // do nothing + } + } + } + + Intent intent = new Intent(Intent.ACTION_SEND, Uri.parse("mailto:")); + intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.app_name) + " - " + encoder.getTitle()); + intent.putExtra(Intent.EXTRA_TEXT, contents); + intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://" + barcodeFile.getAbsolutePath())); + intent.setType("image/png"); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + startActivity(Intent.createChooser(intent, null)); + } + + private static CharSequence makeBarcodeFileName(CharSequence contents) { + String fileName = NOT_ALPHANUMERIC.matcher(contents).replaceAll("_"); + if (fileName.length() > MAX_BARCODE_FILENAME_LENGTH) { + fileName = fileName.substring(0, MAX_BARCODE_FILENAME_LENGTH); + } + return fileName; + } + + @Override + protected void onResume() { + super.onResume(); + // This assumes the view is full screen, which is a good assumption + WindowManager manager = (WindowManager) getSystemService(WINDOW_SERVICE); + Display display = manager.getDefaultDisplay(); + Point displaySize = new Point(); + display.getSize(displaySize); + int width = displaySize.x; + int height = displaySize.y; + int smallerDimension = width < height ? width : height; + smallerDimension = smallerDimension * 7 / 8; + + Intent intent = getIntent(); + if (intent == null) { + return; + } + + try { + boolean useVCard = intent.getBooleanExtra(USE_VCARD_KEY, false); + qrCodeEncoder = new QRCodeEncoder(this, intent, smallerDimension, useVCard); + Bitmap bitmap = qrCodeEncoder.encodeAsBitmap(); + if (bitmap == null) { + Log.w(TAG, "Could not encode barcode"); + showErrorMessage(R.string.msg_encode_contents_failed); + qrCodeEncoder = null; + return; + } + + ImageView view = (ImageView) findViewById(R.id.image_view); + view.setImageBitmap(bitmap); + + TextView contents = (TextView) findViewById(R.id.contents_text_view); + if (intent.getBooleanExtra(Intents.Encode.SHOW_CONTENTS, true)) { + contents.setText(qrCodeEncoder.getDisplayContents()); + setTitle(qrCodeEncoder.getTitle()); + } else { + contents.setText(""); + setTitle(""); + } + } catch (WriterException e) { + Log.w(TAG, "Could not encode barcode", e); + showErrorMessage(R.string.msg_encode_contents_failed); + qrCodeEncoder = null; + } + } + + private void showErrorMessage(int message) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setMessage(message); + builder.setPositiveButton(R.string.button_ok, new FinishListener(this)); + builder.setOnCancelListener(new FinishListener(this)); + builder.show(); + } +} diff --git a/app/src/main/java/com/google/zxing/client/android/encode/Formatter.java b/app/src/main/java/com/google/zxing/client/android/encode/Formatter.java new file mode 100644 index 0000000..9e0ae85 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/encode/Formatter.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2011 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.encode; + +/** + * Encapsulates some simple formatting logic, to aid refactoring in {@link ContactEncoder}. + * + * @author Sean Owen + */ +interface Formatter { + + /** + * @param value value to format + * @param index index of value in a list of values to be formatted + * @return formatted value + */ + CharSequence format(CharSequence value, int index); + +} diff --git a/app/src/main/java/com/google/zxing/client/android/encode/MECARDContactEncoder.java b/app/src/main/java/com/google/zxing/client/android/encode/MECARDContactEncoder.java new file mode 100644 index 0000000..f41d854 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/encode/MECARDContactEncoder.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2011 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.encode; + +import android.telephony.PhoneNumberUtils; + +import java.util.List; +import java.util.regex.Pattern; + +/** + * Encodes contact information according to the MECARD format. + * + * @author Sean Owen + */ +final class MECARDContactEncoder extends ContactEncoder { + + private static final char TERMINATOR = ';'; + + @Override + public String[] encode(List names, + String organization, + List addresses, + List phones, + List phoneTypes, + List emails, + List urls, + String note) { + StringBuilder newContents = new StringBuilder(100); + newContents.append("MECARD:"); + + StringBuilder newDisplayContents = new StringBuilder(100); + + Formatter fieldFormatter = new MECARDFieldFormatter(); + + appendUpToUnique(newContents, newDisplayContents, "N", names, 1, new + MECARDNameDisplayFormatter(), fieldFormatter, TERMINATOR); + + append(newContents, newDisplayContents, "ORG", organization, fieldFormatter, TERMINATOR); + + appendUpToUnique(newContents, newDisplayContents, "ADR", addresses, 1, null, fieldFormatter, TERMINATOR); + + appendUpToUnique(newContents, newDisplayContents, "TEL", phones, Integer.MAX_VALUE, + new MECARDTelDisplayFormatter(), fieldFormatter, TERMINATOR); + + appendUpToUnique(newContents, newDisplayContents, "EMAIL", emails, Integer.MAX_VALUE, null, + fieldFormatter, TERMINATOR); + + appendUpToUnique(newContents, newDisplayContents, "URL", urls, Integer.MAX_VALUE, null, + fieldFormatter, TERMINATOR); + + append(newContents, newDisplayContents, "NOTE", note, fieldFormatter, TERMINATOR); + + newContents.append(';'); + + return new String[] { newContents.toString(), newDisplayContents.toString() }; + } + + private static class MECARDFieldFormatter implements Formatter { + private static final Pattern RESERVED_MECARD_CHARS = Pattern.compile("([\\\\:;])"); + private static final Pattern NEWLINE = Pattern.compile("\\n"); + @Override + public CharSequence format(CharSequence value, int index) { + return ':' + NEWLINE.matcher(RESERVED_MECARD_CHARS.matcher(value).replaceAll("\\\\$1")).replaceAll(""); + } + } + + private static class MECARDTelDisplayFormatter implements Formatter { + private static final Pattern NOT_DIGITS = Pattern.compile("[^0-9]+"); + @Override + public CharSequence format(CharSequence value, int index) { + return NOT_DIGITS.matcher(PhoneNumberUtils.formatNumber(value.toString())).replaceAll(""); + } + } + + private static class MECARDNameDisplayFormatter implements Formatter { + private static final Pattern COMMA = Pattern.compile(","); + @Override + public CharSequence format(CharSequence value, int index) { + return COMMA.matcher(value).replaceAll(""); + } + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/encode/QRCodeEncoder.java b/app/src/main/java/com/google/zxing/client/android/encode/QRCodeEncoder.java new file mode 100644 index 0000000..3bc98db --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/encode/QRCodeEncoder.java @@ -0,0 +1,390 @@ +/* + * Copyright (C) 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.encode; + +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Bundle; +import android.provider.ContactsContract; +import android.telephony.PhoneNumberUtils; +import android.util.Log; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.MultiFormatWriter; +import com.google.zxing.Result; +import com.google.zxing.WriterException; +import com.google.zxing.client.android.Contents; +import com.google.zxing.client.android.Intents; +import com.google.zxing.client.result.AddressBookParsedResult; +import com.google.zxing.client.result.ParsedResult; +import com.google.zxing.client.result.ResultParser; +import com.google.zxing.common.BitMatrix; + +import net.foucry.pilldroid.R; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +/** + * This class does the work of decoding the user's request and extracting all the data + * to be encoded in a barcode. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +final class QRCodeEncoder { + + private static final String TAG = QRCodeEncoder.class.getSimpleName(); + + private static final int WHITE = 0xFFFFFFFF; + private static final int BLACK = 0xFF000000; + + private final Context activity; + private String contents; + private String displayContents; + private String title; + private BarcodeFormat format; + private final int dimension; + private final boolean useVCard; + + QRCodeEncoder(Context activity, Intent intent, int dimension, boolean useVCard) throws WriterException { + this.activity = activity; + this.dimension = dimension; + this.useVCard = useVCard; + String action = intent.getAction(); + if (Intents.Encode.ACTION.equals(action)) { + encodeContentsFromZXingIntent(intent); + } else if (Intent.ACTION_SEND.equals(action)) { + encodeContentsFromShareIntent(intent); + } + } + + String getContents() { + return contents; + } + + String getDisplayContents() { + return displayContents; + } + + String getTitle() { + return title; + } + + boolean isUseVCard() { + return useVCard; + } + + // It would be nice if the string encoding lived in the core ZXing library, + // but we use platform specific code like PhoneNumberUtils, so it can't. + private boolean encodeContentsFromZXingIntent(Intent intent) { + // Default to QR_CODE if no format given. + String formatString = intent.getStringExtra(Intents.Encode.FORMAT); + format = null; + if (formatString != null) { + try { + format = BarcodeFormat.valueOf(formatString); + } catch (IllegalArgumentException iae) { + // Ignore it then + } + } + if (format == null || format == BarcodeFormat.QR_CODE) { + String type = intent.getStringExtra(Intents.Encode.TYPE); + if (type == null || type.isEmpty()) { + return false; + } + this.format = BarcodeFormat.QR_CODE; + encodeQRCodeContents(intent, type); + } else { + String data = intent.getStringExtra(Intents.Encode.DATA); + if (data != null && !data.isEmpty()) { + contents = data; + displayContents = data; + title = activity.getString(R.string.contents_text); + } + } + return contents != null && !contents.isEmpty(); + } + + // Handles send intents from multitude of Android applications + private void encodeContentsFromShareIntent(Intent intent) throws WriterException { + // Check if this is a plain text encoding, or contact + if (intent.hasExtra(Intent.EXTRA_STREAM)) { + encodeFromStreamExtra(intent); + } else { + encodeFromTextExtras(intent); + } + } + + private void encodeFromTextExtras(Intent intent) throws WriterException { + // Notice: Google Maps shares both URL and details in one text, bummer! + String theContents = ContactEncoder.trim(intent.getStringExtra(Intent.EXTRA_TEXT)); + if (theContents == null) { + theContents = ContactEncoder.trim(intent.getStringExtra("android.intent.extra.HTML_TEXT")); + // Intent.EXTRA_HTML_TEXT + if (theContents == null) { + theContents = ContactEncoder.trim(intent.getStringExtra(Intent.EXTRA_SUBJECT)); + if (theContents == null) { + String[] emails = intent.getStringArrayExtra(Intent.EXTRA_EMAIL); + if (emails != null) { + theContents = ContactEncoder.trim(emails[0]); + } else { + theContents = "?"; + } + } + } + } + + // Trim text to avoid URL breaking. + if (theContents == null || theContents.isEmpty()) { + throw new WriterException("Empty EXTRA_TEXT"); + } + contents = theContents; + // We only do QR code. + format = BarcodeFormat.QR_CODE; + if (intent.hasExtra(Intent.EXTRA_SUBJECT)) { + displayContents = intent.getStringExtra(Intent.EXTRA_SUBJECT); + } else if (intent.hasExtra(Intent.EXTRA_TITLE)) { + displayContents = intent.getStringExtra(Intent.EXTRA_TITLE); + } else { + displayContents = contents; + } + title = activity.getString(R.string.contents_text); + } + + // Handles send intents from the Contacts app, retrieving a contact as a VCARD. + private void encodeFromStreamExtra(Intent intent) throws WriterException { + format = BarcodeFormat.QR_CODE; + Bundle bundle = intent.getExtras(); + if (bundle == null) { + throw new WriterException("No extras"); + } + Uri uri = bundle.getParcelable(Intent.EXTRA_STREAM); + if (uri == null) { + throw new WriterException("No EXTRA_STREAM"); + } + byte[] vcard; + String vcardString; + InputStream stream = null; + try { + stream = activity.getContentResolver().openInputStream(uri); + if (stream == null) { + throw new WriterException("Can't open stream for " + uri); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[2048]; + int bytesRead; + while ((bytesRead = stream.read(buffer)) > 0) { + baos.write(buffer, 0, bytesRead); + } + vcard = baos.toByteArray(); + vcardString = new String(vcard, 0, vcard.length, "UTF-8"); + } catch (IOException ioe) { + throw new WriterException(ioe); + } finally { + if (stream != null) { + try { + stream.close(); + } catch (IOException e) { + // continue + } + } + } + Log.d(TAG, "Encoding share intent content:"); + Log.d(TAG, vcardString); + Result result = new Result(vcardString, vcard, null, BarcodeFormat.QR_CODE); + ParsedResult parsedResult = ResultParser.parseResult(result); + if (!(parsedResult instanceof AddressBookParsedResult)) { + throw new WriterException("Result was not an address"); + } + encodeQRCodeContents((AddressBookParsedResult) parsedResult); + if (contents == null || contents.isEmpty()) { + throw new WriterException("No content to encode"); + } + } + + private void encodeQRCodeContents(Intent intent, String type) { + switch (type) { + case Contents.Type.TEXT: + String textData = intent.getStringExtra(Intents.Encode.DATA); + if (textData != null && !textData.isEmpty()) { + contents = textData; + displayContents = textData; + title = activity.getString(R.string.contents_text); + } + break; + + case Contents.Type.EMAIL: + String emailData = ContactEncoder.trim(intent.getStringExtra(Intents.Encode.DATA)); + if (emailData != null) { + contents = "mailto:" + emailData; + displayContents = emailData; + title = activity.getString(R.string.contents_email); + } + break; + + case Contents.Type.PHONE: + String phoneData = ContactEncoder.trim(intent.getStringExtra(Intents.Encode.DATA)); + if (phoneData != null) { + contents = "tel:" + phoneData; + displayContents = PhoneNumberUtils.formatNumber(phoneData); + title = activity.getString(R.string.contents_phone); + } + break; + + case Contents.Type.SMS: + String smsData = ContactEncoder.trim(intent.getStringExtra(Intents.Encode.DATA)); + if (smsData != null) { + contents = "sms:" + smsData; + displayContents = PhoneNumberUtils.formatNumber(smsData); + title = activity.getString(R.string.contents_sms); + } + break; + + case Contents.Type.CONTACT: + Bundle contactBundle = intent.getBundleExtra(Intents.Encode.DATA); + if (contactBundle != null) { + + String name = contactBundle.getString(ContactsContract.Intents.Insert.NAME); + String organization = contactBundle.getString(ContactsContract.Intents.Insert.COMPANY); + String address = contactBundle.getString(ContactsContract.Intents.Insert.POSTAL); + List phones = getAllBundleValues(contactBundle, Contents.PHONE_KEYS); + List phoneTypes = getAllBundleValues(contactBundle, Contents.PHONE_TYPE_KEYS); + List emails = getAllBundleValues(contactBundle, Contents.EMAIL_KEYS); + String url = contactBundle.getString(Contents.URL_KEY); + List urls = url == null ? null : Collections.singletonList(url); + String note = contactBundle.getString(Contents.NOTE_KEY); + + ContactEncoder encoder = useVCard ? new VCardContactEncoder() : new MECARDContactEncoder(); + String[] encoded = encoder.encode(Collections.singletonList(name), + organization, + Collections.singletonList(address), + phones, + phoneTypes, + emails, + urls, + note); + // Make sure we've encoded at least one field. + if (!encoded[1].isEmpty()) { + contents = encoded[0]; + displayContents = encoded[1]; + title = activity.getString(R.string.contents_contact); + } + + } + break; + + case Contents.Type.LOCATION: + Bundle locationBundle = intent.getBundleExtra(Intents.Encode.DATA); + if (locationBundle != null) { + // These must use Bundle.getFloat(), not getDouble(), it's part of the API. + float latitude = locationBundle.getFloat("LAT", Float.MAX_VALUE); + float longitude = locationBundle.getFloat("LONG", Float.MAX_VALUE); + if (latitude != Float.MAX_VALUE && longitude != Float.MAX_VALUE) { + contents = "geo:" + latitude + ',' + longitude; + displayContents = latitude + "," + longitude; + title = activity.getString(R.string.contents_location); + } + } + break; + } + } + + private static List getAllBundleValues(Bundle bundle, String[] keys) { + List values = new ArrayList<>(keys.length); + for (String key : keys) { + Object value = bundle.get(key); + values.add(value == null ? null : value.toString()); + } + return values; + } + + private void encodeQRCodeContents(AddressBookParsedResult contact) { + ContactEncoder encoder = useVCard ? new VCardContactEncoder() : new MECARDContactEncoder(); + String[] encoded = encoder.encode(toList(contact.getNames()), + contact.getOrg(), + toList(contact.getAddresses()), + toList(contact.getPhoneNumbers()), + null, + toList(contact.getEmails()), + toList(contact.getURLs()), + null); + // Make sure we've encoded at least one field. + if (!encoded[1].isEmpty()) { + contents = encoded[0]; + displayContents = encoded[1]; + title = activity.getString(R.string.contents_contact); + } + } + + private static List toList(String[] values) { + return values == null ? null : Arrays.asList(values); + } + + Bitmap encodeAsBitmap() throws WriterException { + String contentsToEncode = contents; + if (contentsToEncode == null) { + return null; + } + Map hints = null; + String encoding = guessAppropriateEncoding(contentsToEncode); + if (encoding != null) { + hints = new EnumMap<>(EncodeHintType.class); + hints.put(EncodeHintType.CHARACTER_SET, encoding); + } + BitMatrix result; + try { + result = new MultiFormatWriter().encode(contentsToEncode, format, dimension, dimension, hints); + } catch (IllegalArgumentException iae) { + // Unsupported format + return null; + } + int width = result.getWidth(); + int height = result.getHeight(); + int[] pixels = new int[width * height]; + for (int y = 0; y < height; y++) { + int offset = y * width; + for (int x = 0; x < width; x++) { + pixels[offset + x] = result.get(x, y) ? BLACK : WHITE; + } + } + + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + bitmap.setPixels(pixels, 0, width, 0, 0, width, height); + return bitmap; + } + + private static String guessAppropriateEncoding(CharSequence contents) { + // Very crude at the moment + for (int i = 0; i < contents.length(); i++) { + if (contents.charAt(i) > 0xFF) { + return "UTF-8"; + } + } + return null; + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/encode/VCardContactEncoder.java b/app/src/main/java/com/google/zxing/client/android/encode/VCardContactEncoder.java new file mode 100644 index 0000000..56ced33 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/encode/VCardContactEncoder.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2011 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.encode; + +import android.provider.ContactsContract; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Encodes contact information according to the vCard format. + * + * @author Sean Owen + */ +final class VCardContactEncoder extends ContactEncoder { + + private static final char TERMINATOR = '\n'; + + @Override + public String[] encode(List names, + String organization, + List addresses, + List phones, + List phoneTypes, + List emails, + List urls, + String note) { + StringBuilder newContents = new StringBuilder(100); + newContents.append("BEGIN:VCARD").append(TERMINATOR); + newContents.append("VERSION:3.0").append(TERMINATOR); + + StringBuilder newDisplayContents = new StringBuilder(100); + + Formatter fieldFormatter = new VCardFieldFormatter(); + + appendUpToUnique(newContents, newDisplayContents, "N", names, 1, null, fieldFormatter, TERMINATOR); + + append(newContents, newDisplayContents, "ORG", organization, fieldFormatter, TERMINATOR); + + appendUpToUnique(newContents, newDisplayContents, "ADR", addresses, 1, null, fieldFormatter, TERMINATOR); + + List>> phoneMetadata = buildPhoneMetadata(phones, phoneTypes); + appendUpToUnique(newContents, newDisplayContents, "TEL", phones, Integer.MAX_VALUE, + new VCardTelDisplayFormatter(phoneMetadata), + new VCardFieldFormatter(phoneMetadata), TERMINATOR); + + appendUpToUnique(newContents, newDisplayContents, "EMAIL", emails, Integer.MAX_VALUE, null, + fieldFormatter, TERMINATOR); + + appendUpToUnique(newContents, newDisplayContents, "URL", urls, Integer.MAX_VALUE, null, + fieldFormatter, TERMINATOR); + + append(newContents, newDisplayContents, "NOTE", note, fieldFormatter, TERMINATOR); + + newContents.append("END:VCARD").append(TERMINATOR); + + return new String[] { newContents.toString(), newDisplayContents.toString() }; + } + + static List>> buildPhoneMetadata(Collection phones, List phoneTypes) { + if (phoneTypes == null || phoneTypes.isEmpty()) { + return null; + } + List>> metadataForIndex = new ArrayList<>(); + for (int i = 0; i < phones.size(); i++) { + if (phoneTypes.size() <= i) { + metadataForIndex.add(null); + } else { + Map> metadata = new HashMap<>(); + metadataForIndex.add(metadata); + Set typeTokens = new HashSet<>(); + metadata.put("TYPE", typeTokens); + String typeString = phoneTypes.get(i); + Integer androidType = maybeIntValue(typeString); + if (androidType == null) { + typeTokens.add(typeString); + } else { + String purpose = vCardPurposeLabelForAndroidType(androidType); + String context = vCardContextLabelForAndroidType(androidType); + if (purpose != null) { + typeTokens.add(purpose); + } + if (context != null) { + typeTokens.add(context); + } + } + } + } + return metadataForIndex; + } + + private static Integer maybeIntValue(String value) { + try { + return Integer.valueOf(value); + } catch (NumberFormatException nfe) { + return null; + } + } + + private static String vCardPurposeLabelForAndroidType(int androidType) { + switch (androidType) { + case ContactsContract.CommonDataKinds.Phone.TYPE_FAX_HOME: + case ContactsContract.CommonDataKinds.Phone.TYPE_FAX_WORK: + case ContactsContract.CommonDataKinds.Phone.TYPE_OTHER_FAX: + return "fax"; + case ContactsContract.CommonDataKinds.Phone.TYPE_PAGER: + case ContactsContract.CommonDataKinds.Phone.TYPE_WORK_PAGER: + return "pager"; + case ContactsContract.CommonDataKinds.Phone.TYPE_TTY_TDD: + return "textphone"; + case ContactsContract.CommonDataKinds.Phone.TYPE_MMS: + return "text"; + default: + return null; + } + } + + private static String vCardContextLabelForAndroidType(int androidType) { + switch (androidType) { + case ContactsContract.CommonDataKinds.Phone.TYPE_HOME: + case ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE: + case ContactsContract.CommonDataKinds.Phone.TYPE_FAX_HOME: + case ContactsContract.CommonDataKinds.Phone.TYPE_PAGER: + return "home"; + case ContactsContract.CommonDataKinds.Phone.TYPE_COMPANY_MAIN: + case ContactsContract.CommonDataKinds.Phone.TYPE_WORK: + case ContactsContract.CommonDataKinds.Phone.TYPE_WORK_MOBILE: + case ContactsContract.CommonDataKinds.Phone.TYPE_FAX_WORK: + case ContactsContract.CommonDataKinds.Phone.TYPE_WORK_PAGER: + return "work"; + default: + return null; + } + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/encode/VCardFieldFormatter.java b/app/src/main/java/com/google/zxing/client/android/encode/VCardFieldFormatter.java new file mode 100644 index 0000000..070a4cd --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/encode/VCardFieldFormatter.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2014 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.encode; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * @author Sean Owen + */ +final class VCardFieldFormatter implements Formatter { + + private static final Pattern RESERVED_VCARD_CHARS = Pattern.compile("([\\\\,;])"); + private static final Pattern NEWLINE = Pattern.compile("\\n"); + + private final List>> metadataForIndex; + + VCardFieldFormatter() { + this(null); + } + + VCardFieldFormatter(List>> metadataForIndex) { + this.metadataForIndex = metadataForIndex; + } + + @Override + public CharSequence format(CharSequence value, int index) { + value = RESERVED_VCARD_CHARS.matcher(value).replaceAll("\\\\$1"); + value = NEWLINE.matcher(value).replaceAll(""); + Map> metadata = + metadataForIndex == null || metadataForIndex.size() <= index ? null : metadataForIndex.get(index); + value = formatMetadata(value, metadata); + return value; + } + + private static CharSequence formatMetadata(CharSequence value, Map> metadata) { + StringBuilder withMetadata = new StringBuilder(); + if (metadata != null) { + for (Map.Entry> metadatum : metadata.entrySet()) { + Set values = metadatum.getValue(); + if (values == null || values.isEmpty()) { + continue; + } + withMetadata.append(';').append(metadatum.getKey()).append('='); + if (values.size() > 1) { + withMetadata.append('"'); + } + Iterator valuesIt = values.iterator(); + withMetadata.append(valuesIt.next()); + while (valuesIt.hasNext()) { + withMetadata.append(',').append(valuesIt.next()); + } + if (values.size() > 1) { + withMetadata.append('"'); + } + } + } + withMetadata.append(':').append(value); + return withMetadata; + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/encode/VCardTelDisplayFormatter.java b/app/src/main/java/com/google/zxing/client/android/encode/VCardTelDisplayFormatter.java new file mode 100644 index 0000000..f5eec9b --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/encode/VCardTelDisplayFormatter.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2014 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.encode; + +import android.telephony.PhoneNumberUtils; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author Sean Owen + */ +final class VCardTelDisplayFormatter implements Formatter { + + private final List>> metadataForIndex; + + VCardTelDisplayFormatter() { + this(null); + } + + VCardTelDisplayFormatter(List>> metadataForIndex) { + this.metadataForIndex = metadataForIndex; + } + + @Override + public CharSequence format(CharSequence value, int index) { + value = PhoneNumberUtils.formatNumber(value.toString()); + Map> metadata = + metadataForIndex == null || metadataForIndex.size() <= index ? null : metadataForIndex.get(index); + value = formatMetadata(value, metadata); + return value; + } + + private static CharSequence formatMetadata(CharSequence value, Map> metadata) { + if (metadata == null || metadata.isEmpty()) { + return value; + } + StringBuilder withMetadata = new StringBuilder(); + for (Map.Entry> metadatum : metadata.entrySet()) { + Set values = metadatum.getValue(); + if (values == null || values.isEmpty()) { + continue; + } + Iterator valuesIt = values.iterator(); + withMetadata.append(valuesIt.next()); + while (valuesIt.hasNext()) { + withMetadata.append(',').append(valuesIt.next()); + } + } + if (withMetadata.length() > 0) { + withMetadata.append(' '); + } + withMetadata.append(value); + return withMetadata; + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/history/DBHelper.java b/app/src/main/java/com/google/zxing/client/android/history/DBHelper.java new file mode 100644 index 0000000..41c40a5 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/history/DBHelper.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.history; + +import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteDatabase; +import android.content.Context; + +/** + * @author Sean Owen + */ +final class DBHelper extends SQLiteOpenHelper { + + private static final int DB_VERSION = 5; + private static final String DB_NAME = "barcode_scanner_history.db"; + static final String TABLE_NAME = "history"; + static final String ID_COL = "id"; + static final String TEXT_COL = "text"; + static final String FORMAT_COL = "format"; + static final String DISPLAY_COL = "display"; + static final String TIMESTAMP_COL = "timestamp"; + static final String DETAILS_COL = "details"; + + DBHelper(Context context) { + super(context, DB_NAME, null, DB_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase sqLiteDatabase) { + sqLiteDatabase.execSQL( + "CREATE TABLE " + TABLE_NAME + " (" + + ID_COL + " INTEGER PRIMARY KEY, " + + TEXT_COL + " TEXT, " + + FORMAT_COL + " TEXT, " + + DISPLAY_COL + " TEXT, " + + TIMESTAMP_COL + " INTEGER, " + + DETAILS_COL + " TEXT);"); + } + + @Override + public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) { + sqLiteDatabase.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); + onCreate(sqLiteDatabase); + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/history/HistoryActivity.java b/app/src/main/java/com/google/zxing/client/android/history/HistoryActivity.java new file mode 100644 index 0000000..080af97 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/history/HistoryActivity.java @@ -0,0 +1,163 @@ +/* + * Copyright 2012 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.history; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.ListActivity; +import android.content.ActivityNotFoundException; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Parcelable; +import android.util.Log; +import android.view.ContextMenu; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import com.google.zxing.client.android.CaptureActivity; +import com.google.zxing.client.android.Intents; + +import net.foucry.pilldroid.R; + +public final class HistoryActivity extends ListActivity { + + private static final String TAG = HistoryActivity.class.getSimpleName(); + + private HistoryManager historyManager; + private ArrayAdapter adapter; + private CharSequence originalTitle; + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + this.historyManager = new HistoryManager(this); + adapter = new HistoryItemAdapter(this); + setListAdapter(adapter); + View listview = getListView(); + registerForContextMenu(listview); + originalTitle = getTitle(); + } + + @Override + protected void onResume() { + super.onResume(); + reloadHistoryItems(); + } + + private void reloadHistoryItems() { + Iterable items = historyManager.buildHistoryItems(); + adapter.clear(); + for (HistoryItem item : items) { + adapter.add(item); + } + setTitle(originalTitle + " (" + adapter.getCount() + ')'); + if (adapter.isEmpty()) { + adapter.add(new HistoryItem(null, null, null)); + } + } + + @Override + protected void onListItemClick(ListView l, View v, int position, long id) { + if (adapter.getItem(position).getResult() != null) { + Intent intent = new Intent(this, CaptureActivity.class); + intent.putExtra(Intents.History.ITEM_NUMBER, position); + setResult(Activity.RESULT_OK, intent); + finish(); + } + } + + @Override + public void onCreateContextMenu(ContextMenu menu, + View v, + ContextMenu.ContextMenuInfo menuInfo) { + int position = ((AdapterView.AdapterContextMenuInfo) menuInfo).position; + if (position >= adapter.getCount() || adapter.getItem(position).getResult() != null) { + menu.add(Menu.NONE, position, position, R.string.history_clear_one_history_text); + } // else it's just that dummy "Empty" message + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + int position = item.getItemId(); + historyManager.deleteHistoryItem(position); + reloadHistoryItems(); + return true; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + if (historyManager.hasHistoryItems()) { + MenuInflater menuInflater = getMenuInflater(); + menuInflater.inflate(R.menu.history, menu); + } + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_history_send: + CharSequence history = historyManager.buildHistory(); + Parcelable historyFile = HistoryManager.saveHistory(history.toString()); + if (historyFile == null) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setMessage(R.string.msg_unmount_usb); + builder.setPositiveButton(R.string.button_ok, null); + builder.show(); + } else { + Intent intent = new Intent(Intent.ACTION_SEND, Uri.parse("mailto:")); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + String subject = getResources().getString(R.string.history_email_title); + intent.putExtra(Intent.EXTRA_SUBJECT, subject); + intent.putExtra(Intent.EXTRA_TEXT, subject); + intent.putExtra(Intent.EXTRA_STREAM, historyFile); + intent.setType("text/csv"); + try { + startActivity(intent); + } catch (ActivityNotFoundException anfe) { + Log.w(TAG, anfe.toString()); + } + } + break; + case R.id.menu_history_clear_text: + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setMessage(R.string.msg_sure); + builder.setCancelable(true); + builder.setPositiveButton(R.string.button_ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int i2) { + historyManager.clearHistory(); + dialog.dismiss(); + finish(); + } + }); + builder.setNegativeButton(R.string.button_cancel, null); + builder.show(); + break; + default: + return super.onOptionsItemSelected(item); + } + return true; + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/history/HistoryItem.java b/app/src/main/java/com/google/zxing/client/android/history/HistoryItem.java new file mode 100644 index 0000000..45da8bd --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/history/HistoryItem.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.history; + +import com.google.zxing.Result; + +public final class HistoryItem { + + private final Result result; + private final String display; + private final String details; + + HistoryItem(Result result, String display, String details) { + this.result = result; + this.display = display; + this.details = details; + } + + public Result getResult() { + return result; + } + + public String getDisplayAndDetails() { + StringBuilder displayResult = new StringBuilder(); + if (display == null || display.isEmpty()) { + displayResult.append(result.getText()); + } else { + displayResult.append(display); + } + if (details != null && !details.isEmpty()) { + displayResult.append(" : ").append(details); + } + return displayResult.toString(); + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/history/HistoryItemAdapter.java b/app/src/main/java/com/google/zxing/client/android/history/HistoryItemAdapter.java new file mode 100644 index 0000000..762ba2d --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/history/HistoryItemAdapter.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.history; + +import android.content.Context; +import android.content.res.Resources; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.LinearLayout; +import android.widget.TextView; +import com.google.zxing.Result; +import net.foucry.pilldroid.R; + +import java.util.ArrayList; + +final class HistoryItemAdapter extends ArrayAdapter { + + private final Context activity; + + HistoryItemAdapter(Context activity) { + super(activity, R.layout.history_list_item, new ArrayList()); + this.activity = activity; + } + + @Override + public View getView(int position, View view, ViewGroup viewGroup) { + View layout; + if (view instanceof LinearLayout) { + layout = view; + } else { + LayoutInflater factory = LayoutInflater.from(activity); + layout = factory.inflate(R.layout.history_list_item, viewGroup, false); + } + + HistoryItem item = getItem(position); + Result result = item.getResult(); + + CharSequence title; + CharSequence detail; + if (result != null) { + title = result.getText(); + detail = item.getDisplayAndDetails(); + } else { + Resources resources = getContext().getResources(); + title = resources.getString(R.string.history_empty); + detail = resources.getString(R.string.history_empty_detail); + } + + ((TextView) layout.findViewById(R.id.history_title)).setText(title); + ((TextView) layout.findViewById(R.id.history_detail)).setText(detail); + + return layout; + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/history/HistoryManager.java b/app/src/main/java/com/google/zxing/client/android/history/HistoryManager.java new file mode 100644 index 0000000..dbb7934 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/history/HistoryManager.java @@ -0,0 +1,366 @@ +/* + * Copyright (C) 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.history; + +import android.database.sqlite.SQLiteException; +import com.google.zxing.BarcodeFormat; +import com.google.zxing.Result; +import com.google.zxing.client.android.Intents; +import com.google.zxing.client.android.PreferencesActivity; +import com.google.zxing.client.android.result.ResultHandler; + +import android.app.Activity; +import android.content.ContentValues; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.net.Uri; +import android.os.Environment; +import android.preference.PreferenceManager; +import android.util.Log; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.charset.Charset; +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + *

Manages functionality related to scan history.

+ * + * @author Sean Owen + */ +public final class HistoryManager { + + private static final String TAG = HistoryManager.class.getSimpleName(); + + private static final int MAX_ITEMS = 2000; + + private static final String[] COLUMNS = { + DBHelper.TEXT_COL, + DBHelper.DISPLAY_COL, + DBHelper.FORMAT_COL, + DBHelper.TIMESTAMP_COL, + DBHelper.DETAILS_COL, + }; + + private static final String[] COUNT_COLUMN = { "COUNT(1)" }; + + private static final String[] ID_COL_PROJECTION = { DBHelper.ID_COL }; + private static final String[] ID_DETAIL_COL_PROJECTION = { DBHelper.ID_COL, DBHelper.DETAILS_COL }; + + private final Activity activity; + private final boolean enableHistory; + + public HistoryManager(Activity activity) { + this.activity = activity; + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity); + enableHistory = prefs.getBoolean(PreferencesActivity.KEY_ENABLE_HISTORY, true); + } + + public boolean hasHistoryItems() { + SQLiteOpenHelper helper = new DBHelper(activity); + SQLiteDatabase db = null; + Cursor cursor = null; + try { + db = helper.getReadableDatabase(); + cursor = db.query(DBHelper.TABLE_NAME, COUNT_COLUMN, null, null, null, null, null); + cursor.moveToFirst(); + return cursor.getInt(0) > 0; + } finally { + close(cursor, db); + } + } + + public List buildHistoryItems() { + SQLiteOpenHelper helper = new DBHelper(activity); + List items = new ArrayList<>(); + SQLiteDatabase db = null; + Cursor cursor = null; + try { + db = helper.getReadableDatabase(); + cursor = db.query(DBHelper.TABLE_NAME, COLUMNS, null, null, null, null, DBHelper.TIMESTAMP_COL + " DESC"); + while (cursor.moveToNext()) { + String text = cursor.getString(0); + String display = cursor.getString(1); + String format = cursor.getString(2); + long timestamp = cursor.getLong(3); + String details = cursor.getString(4); + Result result = new Result(text, null, null, BarcodeFormat.valueOf(format), timestamp); + items.add(new HistoryItem(result, display, details)); + } + } finally { + close(cursor, db); + } + return items; + } + + public HistoryItem buildHistoryItem(int number) { + SQLiteOpenHelper helper = new DBHelper(activity); + SQLiteDatabase db = null; + Cursor cursor = null; + try { + db = helper.getReadableDatabase(); + cursor = db.query(DBHelper.TABLE_NAME, COLUMNS, null, null, null, null, DBHelper.TIMESTAMP_COL + " DESC"); + cursor.move(number + 1); + String text = cursor.getString(0); + String display = cursor.getString(1); + String format = cursor.getString(2); + long timestamp = cursor.getLong(3); + String details = cursor.getString(4); + Result result = new Result(text, null, null, BarcodeFormat.valueOf(format), timestamp); + return new HistoryItem(result, display, details); + } finally { + close(cursor, db); + } + } + + public void deleteHistoryItem(int number) { + SQLiteOpenHelper helper = new DBHelper(activity); + SQLiteDatabase db = null; + Cursor cursor = null; + try { + db = helper.getWritableDatabase(); + cursor = db.query(DBHelper.TABLE_NAME, + ID_COL_PROJECTION, + null, null, null, null, + DBHelper.TIMESTAMP_COL + " DESC"); + cursor.move(number + 1); + db.delete(DBHelper.TABLE_NAME, DBHelper.ID_COL + '=' + cursor.getString(0), null); + } finally { + close(cursor, db); + } + } + + public void addHistoryItem(Result result, ResultHandler handler) { + // Do not save this item to the history if the preference is turned off, or the contents are + // considered secure. + if (!activity.getIntent().getBooleanExtra(Intents.Scan.SAVE_HISTORY, true) || + handler.areContentsSecure() || !enableHistory) { + return; + } + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity); + if (!prefs.getBoolean(PreferencesActivity.KEY_REMEMBER_DUPLICATES, false)) { + deletePrevious(result.getText()); + } + + ContentValues values = new ContentValues(); + values.put(DBHelper.TEXT_COL, result.getText()); + values.put(DBHelper.FORMAT_COL, result.getBarcodeFormat().toString()); + values.put(DBHelper.DISPLAY_COL, handler.getDisplayContents().toString()); + values.put(DBHelper.TIMESTAMP_COL, System.currentTimeMillis()); + + SQLiteOpenHelper helper = new DBHelper(activity); + SQLiteDatabase db = null; + try { + db = helper.getWritableDatabase(); + // Insert the new entry into the DB. + db.insert(DBHelper.TABLE_NAME, DBHelper.TIMESTAMP_COL, values); + } finally { + close(null, db); + } + } + + public void addHistoryItemDetails(String itemID, String itemDetails) { + // As we're going to do an update only we don't need need to worry + // about the preferences; if the item wasn't saved it won't be udpated + SQLiteOpenHelper helper = new DBHelper(activity); + SQLiteDatabase db = null; + Cursor cursor = null; + try { + db = helper.getWritableDatabase(); + cursor = db.query(DBHelper.TABLE_NAME, + ID_DETAIL_COL_PROJECTION, + DBHelper.TEXT_COL + "=?", + new String[] { itemID }, + null, + null, + DBHelper.TIMESTAMP_COL + " DESC", + "1"); + String oldID = null; + String oldDetails = null; + if (cursor.moveToNext()) { + oldID = cursor.getString(0); + oldDetails = cursor.getString(1); + } + + if (oldID != null) { + String newDetails; + if (oldDetails == null) { + newDetails = itemDetails; + } else if (oldDetails.contains(itemDetails)) { + newDetails = null; + } else { + newDetails = oldDetails + " : " + itemDetails; + } + if (newDetails != null) { + ContentValues values = new ContentValues(); + values.put(DBHelper.DETAILS_COL, newDetails); + db.update(DBHelper.TABLE_NAME, values, DBHelper.ID_COL + "=?", new String[] { oldID }); + } + } + + } finally { + close(cursor, db); + } + } + + private void deletePrevious(String text) { + SQLiteOpenHelper helper = new DBHelper(activity); + SQLiteDatabase db = null; + try { + db = helper.getWritableDatabase(); + db.delete(DBHelper.TABLE_NAME, DBHelper.TEXT_COL + "=?", new String[] { text }); + } finally { + close(null, db); + } + } + + public void trimHistory() { + SQLiteOpenHelper helper = new DBHelper(activity); + SQLiteDatabase db = null; + Cursor cursor = null; + try { + db = helper.getWritableDatabase(); + cursor = db.query(DBHelper.TABLE_NAME, + ID_COL_PROJECTION, + null, null, null, null, + DBHelper.TIMESTAMP_COL + " DESC"); + cursor.move(MAX_ITEMS); + while (cursor.moveToNext()) { + String id = cursor.getString(0); + Log.i(TAG, "Deleting scan history ID " + id); + db.delete(DBHelper.TABLE_NAME, DBHelper.ID_COL + '=' + id, null); + } + } catch (SQLiteException sqle) { + // We're seeing an error here when called in CaptureActivity.onCreate() in rare cases + // and don't understand it. First theory is that it's transient so can be safely ignored. + Log.w(TAG, sqle); + // continue + } finally { + close(cursor, db); + } + } + + /** + *

Builds a text representation of the scanning history. Each scan is encoded on one + * line, terminated by a line break (\r\n). The values in each line are comma-separated, + * and double-quoted. Double-quotes within values are escaped with a sequence of two + * double-quotes. The fields output are:

+ * + *
    + *
  1. Raw text
  2. + *
  3. Display text
  4. + *
  5. Format (e.g. QR_CODE)
  6. + *
  7. Unix timestamp (milliseconds since the epoch)
  8. + *
  9. Formatted version of timestamp
  10. + *
  11. Supplemental info (e.g. price info for a product barcode)
  12. + *
+ */ + CharSequence buildHistory() { + SQLiteOpenHelper helper = new DBHelper(activity); + SQLiteDatabase db = null; + Cursor cursor = null; + try { + db = helper.getWritableDatabase(); + cursor = db.query(DBHelper.TABLE_NAME, + COLUMNS, + null, null, null, null, + DBHelper.TIMESTAMP_COL + " DESC"); + + DateFormat format = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM); + StringBuilder historyText = new StringBuilder(1000); + while (cursor.moveToNext()) { + + historyText.append('"').append(massageHistoryField(cursor.getString(0))).append("\","); + historyText.append('"').append(massageHistoryField(cursor.getString(1))).append("\","); + historyText.append('"').append(massageHistoryField(cursor.getString(2))).append("\","); + historyText.append('"').append(massageHistoryField(cursor.getString(3))).append("\","); + + // Add timestamp again, formatted + long timestamp = cursor.getLong(3); + historyText.append('"').append(massageHistoryField( + format.format(new Date(timestamp)))).append("\","); + + // Above we're preserving the old ordering of columns which had formatted data in position 5 + + historyText.append('"').append(massageHistoryField(cursor.getString(4))).append("\"\r\n"); + } + return historyText; + } finally { + close(cursor, db); + } + } + + void clearHistory() { + SQLiteOpenHelper helper = new DBHelper(activity); + SQLiteDatabase db = null; + try { + db = helper.getWritableDatabase(); + db.delete(DBHelper.TABLE_NAME, null, null); + } finally { + close(null, db); + } + } + + static Uri saveHistory(String history) { + File bsRoot = new File(Environment.getExternalStorageDirectory(), "BarcodeScanner"); + File historyRoot = new File(bsRoot, "History"); + if (!historyRoot.exists() && !historyRoot.mkdirs()) { + Log.w(TAG, "Couldn't make dir " + historyRoot); + return null; + } + File historyFile = new File(historyRoot, "history-" + System.currentTimeMillis() + ".csv"); + OutputStreamWriter out = null; + try { + out = new OutputStreamWriter(new FileOutputStream(historyFile), Charset.forName("UTF-8")); + out.write(history); + return Uri.parse("file://" + historyFile.getAbsolutePath()); + } catch (IOException ioe) { + Log.w(TAG, "Couldn't access file " + historyFile + " due to " + ioe); + return null; + } finally { + if (out != null) { + try { + out.close(); + } catch (IOException ioe) { + // do nothing + } + } + } + } + + private static String massageHistoryField(String value) { + return value == null ? "" : value.replace("\"","\"\""); + } + + private static void close(Cursor cursor, SQLiteDatabase database) { + if (cursor != null) { + cursor.close(); + } + if (database != null) { + database.close(); + } + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/result/AddressBookResultHandler.java b/app/src/main/java/com/google/zxing/client/android/result/AddressBookResultHandler.java new file mode 100644 index 0000000..4c69e3a --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/result/AddressBookResultHandler.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.result; + +import android.app.Activity; +import android.graphics.Typeface; +import android.telephony.PhoneNumberUtils; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.StyleSpan; + +import com.google.zxing.client.result.AddressBookParsedResult; +import com.google.zxing.client.result.ParsedResult; + +import net.foucry.pilldroid.R; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +/** + * Handles address book entries. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class AddressBookResultHandler extends ResultHandler { + + private static final DateFormat[] DATE_FORMATS = { + new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH), + new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.ENGLISH), + new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH), + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH), + }; + static { + for (DateFormat format : DATE_FORMATS) { + format.setLenient(false); + } + } + + private static final int[] BUTTON_TEXTS = { + R.string.button_add_contact, + R.string.button_show_map, + R.string.button_dial, + R.string.button_email, + }; + + private final boolean[] fields; + private int buttonCount; + + // This takes all the work out of figuring out which buttons/actions should be in which + // positions, based on which fields are present in this barcode. + private int mapIndexToAction(int index) { + if (index < buttonCount) { + int count = -1; + for (int x = 0; x < MAX_BUTTON_COUNT; x++) { + if (fields[x]) { + count++; + } + if (count == index) { + return x; + } + } + } + return -1; + } + + public AddressBookResultHandler(Activity activity, ParsedResult result) { + super(activity, result); + AddressBookParsedResult addressResult = (AddressBookParsedResult) result; + String[] addresses = addressResult.getAddresses(); + boolean hasAddress = addresses != null && addresses.length > 0 && addresses[0] != null && !addresses[0].isEmpty(); + String[] phoneNumbers = addressResult.getPhoneNumbers(); + boolean hasPhoneNumber = phoneNumbers != null && phoneNumbers.length > 0; + String[] emails = addressResult.getEmails(); + boolean hasEmailAddress = emails != null && emails.length > 0; + + fields = new boolean[MAX_BUTTON_COUNT]; + fields[0] = true; // Add contact is always available + fields[1] = hasAddress; + fields[2] = hasPhoneNumber; + fields[3] = hasEmailAddress; + + buttonCount = 0; + for (int x = 0; x < MAX_BUTTON_COUNT; x++) { + if (fields[x]) { + buttonCount++; + } + } + } + + @Override + public int getButtonCount() { + return buttonCount; + } + + @Override + public int getButtonText(int index) { + return BUTTON_TEXTS[mapIndexToAction(index)]; + } + + @Override + public void handleButtonPress(int index) { + AddressBookParsedResult addressResult = (AddressBookParsedResult) getResult(); + String[] addresses = addressResult.getAddresses(); + String address1 = addresses == null || addresses.length < 1 ? null : addresses[0]; + String[] addressTypes = addressResult.getAddressTypes(); + String address1Type = addressTypes == null || addressTypes.length < 1 ? null : addressTypes[0]; + int action = mapIndexToAction(index); + switch (action) { + case 0: + addContact(addressResult.getNames(), + addressResult.getNicknames(), + addressResult.getPronunciation(), + addressResult.getPhoneNumbers(), + addressResult.getPhoneTypes(), + addressResult.getEmails(), + addressResult.getEmailTypes(), + addressResult.getNote(), + addressResult.getInstantMessenger(), + address1, + address1Type, + addressResult.getOrg(), + addressResult.getTitle(), + addressResult.getURLs(), + addressResult.getBirthday(), + addressResult.getGeo()); + break; + case 1: + searchMap(address1); + break; + case 2: + dialPhone(addressResult.getPhoneNumbers()[0]); + break; + case 3: + sendEmail(addressResult.getEmails(), null, null, null, null); + break; + default: + break; + } + } + + private static Date parseDate(String s) { + for (DateFormat currentFormat : DATE_FORMATS) { + try { + return currentFormat.parse(s); + } catch (ParseException e) { + // continue + } + } + return null; + } + + // Overriden so we can hyphenate phone numbers, format birthdays, and bold the name. + @Override + public CharSequence getDisplayContents() { + AddressBookParsedResult result = (AddressBookParsedResult) getResult(); + StringBuilder contents = new StringBuilder(100); + ParsedResult.maybeAppend(result.getNames(), contents); + int namesLength = contents.length(); + + String pronunciation = result.getPronunciation(); + if (pronunciation != null && !pronunciation.isEmpty()) { + contents.append("\n("); + contents.append(pronunciation); + contents.append(')'); + } + + ParsedResult.maybeAppend(result.getTitle(), contents); + ParsedResult.maybeAppend(result.getOrg(), contents); + ParsedResult.maybeAppend(result.getAddresses(), contents); + String[] numbers = result.getPhoneNumbers(); + if (numbers != null) { + for (String number : numbers) { + if (number != null) { + ParsedResult.maybeAppend(PhoneNumberUtils.formatNumber(number), contents); + } + } + } + ParsedResult.maybeAppend(result.getEmails(), contents); + ParsedResult.maybeAppend(result.getURLs(), contents); + + String birthday = result.getBirthday(); + if (birthday != null && !birthday.isEmpty()) { + Date date = parseDate(birthday); + if (date != null) { + ParsedResult.maybeAppend(DateFormat.getDateInstance(DateFormat.MEDIUM).format(date.getTime()), contents); + } + } + ParsedResult.maybeAppend(result.getNote(), contents); + + if (namesLength > 0) { + // Bold the full name to make it stand out a bit. + Spannable styled = new SpannableString(contents.toString()); + styled.setSpan(new StyleSpan(Typeface.BOLD), 0, namesLength, 0); + return styled; + } else { + return contents.toString(); + } + } + + @Override + public int getDisplayTitle() { + return R.string.result_address_book; + } +} diff --git a/app/src/main/java/com/google/zxing/client/android/result/CalendarResultHandler.java b/app/src/main/java/com/google/zxing/client/android/result/CalendarResultHandler.java new file mode 100644 index 0000000..31438df --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/result/CalendarResultHandler.java @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.result; + +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.Intent; +import android.util.Log; + +import com.google.zxing.client.result.CalendarParsedResult; +import com.google.zxing.client.result.ParsedResult; + +import net.foucry.pilldroid.R; + +import java.text.DateFormat; +import java.util.Date; + +/** + * Handles calendar entries encoded in QR Codes. + * + * @author dswitkin@google.com (Daniel Switkin) + * @author Sean Owen + */ +public final class CalendarResultHandler extends ResultHandler { + + private static final String TAG = CalendarResultHandler.class.getSimpleName(); + + private static final int[] buttons = { + R.string.button_add_calendar + }; + + public CalendarResultHandler(Activity activity, ParsedResult result) { + super(activity, result); + } + + @Override + public int getButtonCount() { + return buttons.length; + } + + @Override + public int getButtonText(int index) { + return buttons[index]; + } + + @Override + public void handleButtonPress(int index) { + if (index == 0) { + CalendarParsedResult calendarResult = (CalendarParsedResult) getResult(); + + String description = calendarResult.getDescription(); + String organizer = calendarResult.getOrganizer(); + if (organizer != null) { // No separate Intent key, put in description + if (description == null) { + description = organizer; + } else { + description = description + '\n' + organizer; + } + } + + addCalendarEvent(calendarResult.getSummary(), + calendarResult.getStart(), + calendarResult.isStartAllDay(), + calendarResult.getEnd(), + calendarResult.getLocation(), + description, + calendarResult.getAttendees()); + } + } + + /** + * Sends an intent to create a new calendar event by prepopulating the Add Event UI. Older + * versions of the system have a bug where the event title will not be filled out. + * + * @param summary A description of the event + * @param start The start time + * @param allDay if true, event is considered to be all day starting from start time + * @param end The end time (optional) + * @param location a text description of the event location + * @param description a text description of the event itself + * @param attendees attendees to invite + */ + private void addCalendarEvent(String summary, + Date start, + boolean allDay, + Date end, + String location, + String description, + String[] attendees) { + Intent intent = new Intent(Intent.ACTION_INSERT); + intent.setType("vnd.android.cursor.item/event"); + long startMilliseconds = start.getTime(); + intent.putExtra("beginTime", startMilliseconds); + if (allDay) { + intent.putExtra("allDay", true); + } + long endMilliseconds; + if (end == null) { + if (allDay) { + // + 1 day + endMilliseconds = startMilliseconds + 24 * 60 * 60 * 1000; + } else { + endMilliseconds = startMilliseconds; + } + } else { + endMilliseconds = end.getTime(); + } + intent.putExtra("endTime", endMilliseconds); + intent.putExtra("title", summary); + intent.putExtra("eventLocation", location); + intent.putExtra("description", description); + if (attendees != null) { + intent.putExtra(Intent.EXTRA_EMAIL, attendees); + // Documentation says this is either a String[] or comma-separated String, which is right? + } + + try { + // Do this manually at first + rawLaunchIntent(intent); + } catch (ActivityNotFoundException anfe) { + Log.w(TAG, "No calendar app available that responds to " + Intent.ACTION_INSERT); + // For calendar apps that don't like "INSERT": + intent.setAction(Intent.ACTION_EDIT); + launchIntent(intent); // Fail here for real if nothing can handle it + } + } + + + @Override + public CharSequence getDisplayContents() { + + CalendarParsedResult calResult = (CalendarParsedResult) getResult(); + StringBuilder result = new StringBuilder(100); + + ParsedResult.maybeAppend(calResult.getSummary(), result); + + Date start = calResult.getStart(); + ParsedResult.maybeAppend(format(calResult.isStartAllDay(), start), result); + + Date end = calResult.getEnd(); + if (end != null) { + if (calResult.isEndAllDay() && !start.equals(end)) { + // Show only year/month/day + // if it's all-day and this is the end date, it's exclusive, so show the user + // that it ends on the day before to make more intuitive sense. + // But don't do it if the event already (incorrectly?) specifies the same start/end + end = new Date(end.getTime() - 24 * 60 * 60 * 1000); + } + ParsedResult.maybeAppend(format(calResult.isEndAllDay(), end), result); + } + + ParsedResult.maybeAppend(calResult.getLocation(), result); + ParsedResult.maybeAppend(calResult.getOrganizer(), result); + ParsedResult.maybeAppend(calResult.getAttendees(), result); + ParsedResult.maybeAppend(calResult.getDescription(), result); + return result.toString(); + } + + private static String format(boolean allDay, Date date) { + if (date == null) { + return null; + } + DateFormat format = allDay + ? DateFormat.getDateInstance(DateFormat.MEDIUM) + : DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM); + return format.format(date); + } + + @Override + public int getDisplayTitle() { + return R.string.result_calendar; + } +} diff --git a/app/src/main/java/com/google/zxing/client/android/result/EmailAddressResultHandler.java b/app/src/main/java/com/google/zxing/client/android/result/EmailAddressResultHandler.java new file mode 100644 index 0000000..61631a3 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/result/EmailAddressResultHandler.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.result; + +import android.app.Activity; + +import com.google.zxing.client.result.EmailAddressParsedResult; +import com.google.zxing.client.result.ParsedResult; + +import net.foucry.pilldroid.R; + +/** + * Handles email addresses. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class EmailAddressResultHandler extends ResultHandler { + private static final int[] buttons = { + R.string.button_email, + R.string.button_add_contact + }; + + public EmailAddressResultHandler(Activity activity, ParsedResult result) { + super(activity, result); + } + + @Override + public int getButtonCount() { + return buttons.length; + } + + @Override + public int getButtonText(int index) { + return buttons[index]; + } + + @Override + public void handleButtonPress(int index) { + EmailAddressParsedResult emailResult = (EmailAddressParsedResult) getResult(); + switch (index) { + case 0: + sendEmail(emailResult.getTos(), + emailResult.getCCs(), + emailResult.getBCCs(), + emailResult.getSubject(), + emailResult.getBody()); + break; + case 1: + addEmailOnlyContact(emailResult.getTos(), null); + break; + } + } + + @Override + public int getDisplayTitle() { + return R.string.result_email_address; + } +} diff --git a/app/src/main/java/com/google/zxing/client/android/result/GeoResultHandler.java b/app/src/main/java/com/google/zxing/client/android/result/GeoResultHandler.java new file mode 100644 index 0000000..6d3d855 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/result/GeoResultHandler.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.result; + +import android.app.Activity; + +import com.google.zxing.client.result.GeoParsedResult; +import com.google.zxing.client.result.ParsedResult; + +import net.foucry.pilldroid.R; + +/** + * Handles geographic coordinates (typically encoded as geo: URLs). + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class GeoResultHandler extends ResultHandler { + private static final int[] buttons = { + R.string.button_show_map, + R.string.button_get_directions + }; + + public GeoResultHandler(Activity activity, ParsedResult result) { + super(activity, result); + } + + @Override + public int getButtonCount() { + return buttons.length; + } + + @Override + public int getButtonText(int index) { + return buttons[index]; + } + + @Override + public void handleButtonPress(int index) { + GeoParsedResult geoResult = (GeoParsedResult) getResult(); + switch (index) { + case 0: + openMap(geoResult.getGeoURI()); + break; + case 1: + getDirections(geoResult.getLatitude(), geoResult.getLongitude()); + break; + } + } + + @Override + public int getDisplayTitle() { + return R.string.result_geo; + } +} diff --git a/app/src/main/java/com/google/zxing/client/android/result/ISBNResultHandler.java b/app/src/main/java/com/google/zxing/client/android/result/ISBNResultHandler.java new file mode 100644 index 0000000..085cadb --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/result/ISBNResultHandler.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.result; + +import android.app.Activity; + +import com.google.zxing.Result; +import com.google.zxing.client.result.ISBNParsedResult; +import com.google.zxing.client.result.ParsedResult; + +import net.foucry.pilldroid.R; + +/** + * Handles books encoded by their ISBN values. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class ISBNResultHandler extends ResultHandler { + private static final int[] buttons = { + R.string.button_product_search, + R.string.button_book_search, + R.string.button_search_book_contents, + R.string.button_custom_product_search + }; + + public ISBNResultHandler(Activity activity, ParsedResult result, Result rawResult) { + super(activity, result, rawResult); + } + + @Override + public int getButtonCount() { + return hasCustomProductSearch() ? buttons.length : buttons.length - 1; + } + + @Override + public int getButtonText(int index) { + return buttons[index]; + } + + @Override + public void handleButtonPress(int index) { + ISBNParsedResult isbnResult = (ISBNParsedResult) getResult(); + switch (index) { + case 0: + openProductSearch(isbnResult.getISBN()); + break; + case 1: + openBookSearch(isbnResult.getISBN()); + break; + case 2: + searchBookContents(isbnResult.getISBN()); + break; + case 3: + openURL(fillInCustomSearchURL(isbnResult.getISBN())); + break; + } + } + + @Override + public int getDisplayTitle() { + return R.string.result_isbn; + } +} diff --git a/app/src/main/java/com/google/zxing/client/android/result/ProductResultHandler.java b/app/src/main/java/com/google/zxing/client/android/result/ProductResultHandler.java new file mode 100644 index 0000000..11772ab --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/result/ProductResultHandler.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.result; + +import android.app.Activity; + +import com.google.zxing.Result; +import com.google.zxing.client.result.ExpandedProductParsedResult; +import com.google.zxing.client.result.ParsedResult; +import com.google.zxing.client.result.ProductParsedResult; + +import net.foucry.pilldroid.R; + +/** + * Handles generic products which are not books. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class ProductResultHandler extends ResultHandler { + private static final int[] buttons = { + R.string.button_product_search, + R.string.button_web_search, + R.string.button_custom_product_search + }; + + public ProductResultHandler(Activity activity, ParsedResult result, Result rawResult) { + super(activity, result, rawResult); + } + + @Override + public int getButtonCount() { + return hasCustomProductSearch() ? buttons.length : buttons.length - 1; + } + + @Override + public int getButtonText(int index) { + return buttons[index]; + } + + @Override + public void handleButtonPress(int index) { + String productID = getProductIDFromResult(getResult()); + switch (index) { + case 0: + openProductSearch(productID); + break; + case 1: + webSearch(productID); + break; + case 2: + openURL(fillInCustomSearchURL(productID)); + break; + } + } + + private static String getProductIDFromResult(ParsedResult rawResult) { + if (rawResult instanceof ProductParsedResult) { + return ((ProductParsedResult) rawResult).getNormalizedProductID(); + } + if (rawResult instanceof ExpandedProductParsedResult) { + return ((ExpandedProductParsedResult) rawResult).getRawText(); + } + throw new IllegalArgumentException(rawResult.getClass().toString()); + } + + @Override + public int getDisplayTitle() { + return R.string.result_product; + } +} diff --git a/app/src/main/java/com/google/zxing/client/android/result/ResultButtonListener.java b/app/src/main/java/com/google/zxing/client/android/result/ResultButtonListener.java new file mode 100644 index 0000000..2e107c4 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/result/ResultButtonListener.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.result; + +import android.view.View; + +/** + * Handles the result of barcode decoding in the context of the Android platform, by dispatching the + * proper intents to open other activities like GMail, Maps, etc. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class ResultButtonListener implements View.OnClickListener { + private final ResultHandler resultHandler; + private final int index; + + public ResultButtonListener(ResultHandler resultHandler, int index) { + this.resultHandler = resultHandler; + this.index = index; + } + + @Override + public void onClick(View view) { + resultHandler.handleButtonPress(index); + } +} diff --git a/app/src/main/java/com/google/zxing/client/android/result/ResultHandler.java b/app/src/main/java/com/google/zxing/client/android/result/ResultHandler.java new file mode 100644 index 0000000..086e102 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/result/ResultHandler.java @@ -0,0 +1,516 @@ +/* + * Copyright (C) 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.result; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.ActivityNotFoundException; +import android.content.ContentValues; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.preference.PreferenceManager; +import android.provider.ContactsContract; +import android.util.Log; + +import com.google.zxing.Result; +import com.google.zxing.client.android.Contents; +import com.google.zxing.client.android.Intents; +import com.google.zxing.client.android.LocaleManager; +import com.google.zxing.client.android.PreferencesActivity; +import com.google.zxing.client.android.book.SearchBookContentsActivity; +import com.google.zxing.client.result.ParsedResult; +import com.google.zxing.client.result.ParsedResultType; +import com.google.zxing.client.result.ResultParser; + +import net.foucry.pilldroid.R; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Locale; + +//import com.google.zxing.client.android.R; + +/** + * A base class for the Android-specific barcode handlers. These allow the app to polymorphically + * suggest the appropriate actions for each data type. + * + * This class also contains a bunch of utility methods to take common actions like opening a URL. + * They could easily be moved into a helper object, but it can't be static because the Activity + * instance is needed to launch an intent. + * + * @author dswitkin@google.com (Daniel Switkin) + * @author Sean Owen + */ +public abstract class ResultHandler { + + private static final String TAG = ResultHandler.class.getSimpleName(); + + private static final String[] EMAIL_TYPE_STRINGS = {"home", "work", "mobile"}; + private static final String[] PHONE_TYPE_STRINGS = {"home", "work", "mobile", "fax", "pager", "main"}; + private static final String[] ADDRESS_TYPE_STRINGS = {"home", "work"}; + private static final int[] EMAIL_TYPE_VALUES = { + ContactsContract.CommonDataKinds.Email.TYPE_HOME, + ContactsContract.CommonDataKinds.Email.TYPE_WORK, + ContactsContract.CommonDataKinds.Email.TYPE_MOBILE, + }; + private static final int[] PHONE_TYPE_VALUES = { + ContactsContract.CommonDataKinds.Phone.TYPE_HOME, + ContactsContract.CommonDataKinds.Phone.TYPE_WORK, + ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE, + ContactsContract.CommonDataKinds.Phone.TYPE_FAX_WORK, + ContactsContract.CommonDataKinds.Phone.TYPE_PAGER, + ContactsContract.CommonDataKinds.Phone.TYPE_MAIN, + }; + private static final int[] ADDRESS_TYPE_VALUES = { + ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME, + ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK, + }; + private static final int NO_TYPE = -1; + + public static final int MAX_BUTTON_COUNT = 4; + + private final ParsedResult result; + private final Activity activity; + private final Result rawResult; + private final String customProductSearch; + + ResultHandler(Activity activity, ParsedResult result) { + this(activity, result, null); + } + + ResultHandler(Activity activity, ParsedResult result, Result rawResult) { + this.result = result; + this.activity = activity; + this.rawResult = rawResult; + this.customProductSearch = parseCustomSearchURL(); + } + + public final ParsedResult getResult() { + return result; + } + + final boolean hasCustomProductSearch() { + return customProductSearch != null; + } + + final Activity getActivity() { + return activity; + } + + /** + * Indicates how many buttons the derived class wants shown. + * + * @return The integer button count. + */ + public abstract int getButtonCount(); + + /** + * The text of the nth action button. + * + * @param index From 0 to getButtonCount() - 1 + * @return The button text as a resource ID + */ + public abstract int getButtonText(int index); + + public Integer getDefaultButtonID() { + return null; + } + + /** + * Execute the action which corresponds to the nth button. + * + * @param index The button that was clicked. + */ + public abstract void handleButtonPress(int index); + + /** + * Some barcode contents are considered secure, and should not be saved to history, copied to + * the clipboard, or otherwise persisted. + * + * @return If true, do not create any permanent record of these contents. + */ + public boolean areContentsSecure() { + return false; + } + + /** + * Create a possibly styled string for the contents of the current barcode. + * + * @return The text to be displayed. + */ + public CharSequence getDisplayContents() { + String contents = result.getDisplayResult(); + return contents.replace("\r", ""); + } + + /** + * A string describing the kind of barcode that was found, e.g. "Found contact info". + * + * @return The resource ID of the string. + */ + public abstract int getDisplayTitle(); + + /** + * A convenience method to get the parsed type. Should not be overridden. + * + * @return The parsed type, e.g. URI or ISBN + */ + public final ParsedResultType getType() { + return result.getType(); + } + + final void addPhoneOnlyContact(String[] phoneNumbers,String[] phoneTypes) { + addContact(null, null, null, phoneNumbers, phoneTypes, null, null, null, null, null, null, null, null, null, null, null); + } + + final void addEmailOnlyContact(String[] emails, String[] emailTypes) { + addContact(null, null, null, null, null, emails, emailTypes, null, null, null, null, null, null, null, null, null); + } + + final void addContact(String[] names, + String[] nicknames, + String pronunciation, + String[] phoneNumbers, + String[] phoneTypes, + String[] emails, + String[] emailTypes, + String note, + String instantMessenger, + String address, + String addressType, + String org, + String title, + String[] urls, + String birthday, + String[] geo) { + + // Only use the first name in the array, if present. + Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT, ContactsContract.Contacts.CONTENT_URI); + intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE); + putExtra(intent, ContactsContract.Intents.Insert.NAME, names != null ? names[0] : null); + + putExtra(intent, ContactsContract.Intents.Insert.PHONETIC_NAME, pronunciation); + + int phoneCount = Math.min(phoneNumbers != null ? phoneNumbers.length : 0, Contents.PHONE_KEYS.length); + for (int x = 0; x < phoneCount; x++) { + putExtra(intent, Contents.PHONE_KEYS[x], phoneNumbers[x]); + if (phoneTypes != null && x < phoneTypes.length) { + int type = toPhoneContractType(phoneTypes[x]); + if (type >= 0) { + intent.putExtra(Contents.PHONE_TYPE_KEYS[x], type); + } + } + } + + int emailCount = Math.min(emails != null ? emails.length : 0, Contents.EMAIL_KEYS.length); + for (int x = 0; x < emailCount; x++) { + putExtra(intent, Contents.EMAIL_KEYS[x], emails[x]); + if (emailTypes != null && x < emailTypes.length) { + int type = toEmailContractType(emailTypes[x]); + if (type >= 0) { + intent.putExtra(Contents.EMAIL_TYPE_KEYS[x], type); + } + } + } + + ArrayList data = new ArrayList<>(); + if (urls != null) { + for (String url : urls) { + if (url != null && !url.isEmpty()) { + ContentValues row = new ContentValues(2); + row.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE); + row.put(ContactsContract.CommonDataKinds.Website.URL, url); + data.add(row); + break; + } + } + } + + if (birthday != null) { + ContentValues row = new ContentValues(3); + row.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE); + row.put(ContactsContract.CommonDataKinds.Event.TYPE, ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY); + row.put(ContactsContract.CommonDataKinds.Event.START_DATE, birthday); + data.add(row); + } + + if (nicknames != null) { + for (String nickname : nicknames) { + if (nickname != null && !nickname.isEmpty()) { + ContentValues row = new ContentValues(3); + row.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Nickname.CONTENT_ITEM_TYPE); + row.put(ContactsContract.CommonDataKinds.Nickname.TYPE, + ContactsContract.CommonDataKinds.Nickname.TYPE_DEFAULT); + row.put(ContactsContract.CommonDataKinds.Nickname.NAME, nickname); + data.add(row); + break; + } + } + } + + if (!data.isEmpty()) { + intent.putParcelableArrayListExtra(ContactsContract.Intents.Insert.DATA, data); + } + + StringBuilder aggregatedNotes = new StringBuilder(); + if (note != null) { + aggregatedNotes.append('\n').append(note); + } + if (geo != null) { + aggregatedNotes.append('\n').append(geo[0]).append(',').append(geo[1]); + } + + if (aggregatedNotes.length() > 0) { + // Remove extra leading '\n' + putExtra(intent, ContactsContract.Intents.Insert.NOTES, aggregatedNotes.substring(1)); + } + + putExtra(intent, ContactsContract.Intents.Insert.IM_HANDLE, instantMessenger); + putExtra(intent, ContactsContract.Intents.Insert.POSTAL, address); + if (addressType != null) { + int type = toAddressContractType(addressType); + if (type >= 0) { + intent.putExtra(ContactsContract.Intents.Insert.POSTAL_TYPE, type); + } + } + putExtra(intent, ContactsContract.Intents.Insert.COMPANY, org); + putExtra(intent, ContactsContract.Intents.Insert.JOB_TITLE, title); + launchIntent(intent); + } + + private static int toEmailContractType(String typeString) { + return doToContractType(typeString, EMAIL_TYPE_STRINGS, EMAIL_TYPE_VALUES); + } + + private static int toPhoneContractType(String typeString) { + return doToContractType(typeString, PHONE_TYPE_STRINGS, PHONE_TYPE_VALUES); + } + + private static int toAddressContractType(String typeString) { + return doToContractType(typeString, ADDRESS_TYPE_STRINGS, ADDRESS_TYPE_VALUES); + } + + private static int doToContractType(String typeString, String[] types, int[] values) { + if (typeString == null) { + return NO_TYPE; + } + for (int i = 0; i < types.length; i++) { + String type = types[i]; + if (typeString.startsWith(type) || typeString.startsWith(type.toUpperCase(Locale.ENGLISH))) { + return values[i]; + } + } + return NO_TYPE; + } + + final void shareByEmail(String contents) { + sendEmail(null, null, null, null, contents); + } + + final void sendEmail(String[] to, + String[] cc, + String[] bcc, + String subject, + String body) { + Intent intent = new Intent(Intent.ACTION_SEND, Uri.parse("mailto:")); + if (to != null && to.length != 0) { + intent.putExtra(Intent.EXTRA_EMAIL, to); + } + if (cc != null && cc.length != 0) { + intent.putExtra(Intent.EXTRA_CC, cc); + } + if (bcc != null && bcc.length != 0) { + intent.putExtra(Intent.EXTRA_BCC, bcc); + } + putExtra(intent, Intent.EXTRA_SUBJECT, subject); + putExtra(intent, Intent.EXTRA_TEXT, body); + intent.setType("text/plain"); + launchIntent(intent); + } + + final void shareBySMS(String contents) { + sendSMSFromUri("smsto:", contents); + } + + final void sendSMS(String phoneNumber, String body) { + sendSMSFromUri("smsto:" + phoneNumber, body); + } + + final void sendSMSFromUri(String uri, String body) { + Intent intent = new Intent(Intent.ACTION_SENDTO, Uri.parse(uri)); + putExtra(intent, "sms_body", body); + // Exit the app once the SMS is sent + intent.putExtra("compose_mode", true); + launchIntent(intent); + } + + final void sendMMS(String phoneNumber, String subject, String body) { + sendMMSFromUri("mmsto:" + phoneNumber, subject, body); + } + + final void sendMMSFromUri(String uri, String subject, String body) { + Intent intent = new Intent(Intent.ACTION_SENDTO, Uri.parse(uri)); + // The Messaging app needs to see a valid subject or else it will treat this an an SMS. + if (subject == null || subject.isEmpty()) { + putExtra(intent, "subject", activity.getString(R.string.msg_default_mms_subject)); + } else { + putExtra(intent, "subject", subject); + } + putExtra(intent, "sms_body", body); + intent.putExtra("compose_mode", true); + launchIntent(intent); + } + + final void dialPhone(String phoneNumber) { + launchIntent(new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + phoneNumber))); + } + + final void dialPhoneFromUri(String uri) { + launchIntent(new Intent(Intent.ACTION_DIAL, Uri.parse(uri))); + } + + final void openMap(String geoURI) { + launchIntent(new Intent(Intent.ACTION_VIEW, Uri.parse(geoURI))); + } + + /** + * Do a geo search using the address as the query. + * + * @param address The address to find + */ + final void searchMap(String address) { + launchIntent(new Intent(Intent.ACTION_VIEW, Uri.parse("geo:0,0?q=" + Uri.encode(address)))); + } + + final void getDirections(double latitude, double longitude) { + launchIntent(new Intent(Intent.ACTION_VIEW, Uri.parse("http://maps.google." + + LocaleManager.getCountryTLD(activity) + "/maps?f=d&daddr=" + latitude + ',' + longitude))); + } + + // Uses the mobile-specific version of Product Search, which is formatted for small screens. + final void openProductSearch(String upc) { + Uri uri = Uri.parse("http://www.google." + LocaleManager.getProductSearchCountryTLD(activity) + + "/m/products?q=" + upc + "&source=zxing"); + launchIntent(new Intent(Intent.ACTION_VIEW, uri)); + } + + final void openBookSearch(String isbn) { + Uri uri = Uri.parse("http://books.google." + LocaleManager.getBookSearchCountryTLD(activity) + + "/books?vid=isbn" + isbn); + launchIntent(new Intent(Intent.ACTION_VIEW, uri)); + } + + final void searchBookContents(String isbnOrUrl) { + Intent intent = new Intent(Intents.SearchBookContents.ACTION); + intent.setClassName(activity, SearchBookContentsActivity.class.getName()); + putExtra(intent, Intents.SearchBookContents.ISBN, isbnOrUrl); + launchIntent(intent); + } + + final void openURL(String url) { + // Strangely, some Android browsers don't seem to register to handle HTTP:// or HTTPS://. + // Lower-case these as it should always be OK to lower-case these schemes. + if (url.startsWith("HTTP://")) { + url = "http" + url.substring(4); + } else if (url.startsWith("HTTPS://")) { + url = "https" + url.substring(5); + } + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + try { + launchIntent(intent); + } catch (ActivityNotFoundException ignored) { + Log.w(TAG, "Nothing available to handle " + intent); + } + } + + final void webSearch(String query) { + Intent intent = new Intent(Intent.ACTION_WEB_SEARCH); + intent.putExtra("query", query); + launchIntent(intent); + } + + /** + * Like {@link #launchIntent(Intent)} but will tell you if it is not handle-able + * via {@link ActivityNotFoundException}. + * + * @throws ActivityNotFoundException + */ + final void rawLaunchIntent(Intent intent) { + if (intent != null) { + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + Log.d(TAG, "Launching intent: " + intent + " with extras: " + intent.getExtras()); + activity.startActivity(intent); + } + } + + /** + * Like {@link #rawLaunchIntent(Intent)} but will show a user dialog if nothing is available to handle. + */ + final void launchIntent(Intent intent) { + try { + rawLaunchIntent(intent); + } catch (ActivityNotFoundException ignored) { + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle(R.string.app_name); + builder.setMessage(R.string.msg_intent_failed); + builder.setPositiveButton(R.string.button_ok, null); + builder.show(); + } + } + + private static void putExtra(Intent intent, String key, String value) { + if (value != null && !value.isEmpty()) { + intent.putExtra(key, value); + } + } + + private String parseCustomSearchURL() { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity); + String customProductSearch = prefs.getString(PreferencesActivity.KEY_CUSTOM_PRODUCT_SEARCH, + null); + if (customProductSearch != null && customProductSearch.trim().isEmpty()) { + return null; + } + return customProductSearch; + } + + final String fillInCustomSearchURL(String text) { + if (customProductSearch == null) { + return text; // ? + } + try { + text = URLEncoder.encode(text, "UTF-8"); + } catch (UnsupportedEncodingException e) { + // can't happen; UTF-8 is always supported. Continue, I guess, without encoding + } + String url = customProductSearch; + if (rawResult != null) { + // Replace %f but only if it doesn't seem to be a hex escape sequence. This remains + // problematic but avoids the more surprising problem of breaking escapes + url = url.replaceFirst("%f(?![0-9a-f])", rawResult.getBarcodeFormat().toString()); + if (url.contains("%t")) { + ParsedResult parsedResultAgain = ResultParser.parseResult(rawResult); + url = url.replace("%t", parsedResultAgain.getType().toString()); + } + } + // Replace %s last as it might contain itself %f or %t + return url.replace("%s", text); + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/result/ResultHandlerFactory.java b/app/src/main/java/com/google/zxing/client/android/result/ResultHandlerFactory.java new file mode 100644 index 0000000..a31d089 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/result/ResultHandlerFactory.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.result; + +import com.google.zxing.Result; +import com.google.zxing.client.android.CaptureActivity; +import com.google.zxing.client.result.ParsedResult; +import com.google.zxing.client.result.ResultParser; + +/** + * Manufactures Android-specific handlers based on the barcode content's type. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class ResultHandlerFactory { + private ResultHandlerFactory() { + } + + public static ResultHandler makeResultHandler(CaptureActivity activity, Result rawResult) { + ParsedResult result = parseResult(rawResult); + switch (result.getType()) { + case ADDRESSBOOK: + return new AddressBookResultHandler(activity, result); + case EMAIL_ADDRESS: + return new EmailAddressResultHandler(activity, result); + case PRODUCT: + return new ProductResultHandler(activity, result, rawResult); + case URI: + return new URIResultHandler(activity, result); + case WIFI: + return new WifiResultHandler(activity, result); + case GEO: + return new GeoResultHandler(activity, result); + case TEL: + return new TelResultHandler(activity, result); + case SMS: + return new SMSResultHandler(activity, result); + case CALENDAR: + return new CalendarResultHandler(activity, result); + case ISBN: + return new ISBNResultHandler(activity, result, rawResult); + default: + return new TextResultHandler(activity, result, rawResult); + } + } + + private static ParsedResult parseResult(Result rawResult) { + return ResultParser.parseResult(rawResult); + } +} diff --git a/app/src/main/java/com/google/zxing/client/android/result/SMSResultHandler.java b/app/src/main/java/com/google/zxing/client/android/result/SMSResultHandler.java new file mode 100644 index 0000000..f1c7a3c --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/result/SMSResultHandler.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.result; + +import android.app.Activity; +import android.telephony.PhoneNumberUtils; + +import com.google.zxing.client.result.ParsedResult; +import com.google.zxing.client.result.SMSParsedResult; + +import net.foucry.pilldroid.R; + +/** + * Handles SMS addresses, offering a choice of composing a new SMS or MMS message. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class SMSResultHandler extends ResultHandler { + private static final int[] buttons = { + R.string.button_sms, + R.string.button_mms + }; + + public SMSResultHandler(Activity activity, ParsedResult result) { + super(activity, result); + } + + @Override + public int getButtonCount() { + return buttons.length; + } + + @Override + public int getButtonText(int index) { + return buttons[index]; + } + + @Override + public void handleButtonPress(int index) { + SMSParsedResult smsResult = (SMSParsedResult) getResult(); + String number = smsResult.getNumbers()[0]; + switch (index) { + case 0: + // Don't know of a way yet to express a SENDTO intent with multiple recipients + sendSMS(number, smsResult.getBody()); + break; + case 1: + sendMMS(number, smsResult.getSubject(), smsResult.getBody()); + break; + } + } + + @Override + public CharSequence getDisplayContents() { + SMSParsedResult smsResult = (SMSParsedResult) getResult(); + String[] rawNumbers = smsResult.getNumbers(); + String[] formattedNumbers = new String[rawNumbers.length]; + for (int i = 0; i < rawNumbers.length; i++) { + formattedNumbers[i] = PhoneNumberUtils.formatNumber(rawNumbers[i]); + } + StringBuilder contents = new StringBuilder(50); + ParsedResult.maybeAppend(formattedNumbers, contents); + ParsedResult.maybeAppend(smsResult.getSubject(), contents); + ParsedResult.maybeAppend(smsResult.getBody(), contents); + return contents.toString(); + } + + @Override + public int getDisplayTitle() { + return R.string.result_sms; + } +} diff --git a/app/src/main/java/com/google/zxing/client/android/result/TelResultHandler.java b/app/src/main/java/com/google/zxing/client/android/result/TelResultHandler.java new file mode 100644 index 0000000..8816c2a --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/result/TelResultHandler.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.result; + +import net.foucry.pilldroid.R; +import com.google.zxing.client.result.ParsedResult; +import com.google.zxing.client.result.TelParsedResult; + +import android.app.Activity; +import android.telephony.PhoneNumberUtils; + +/** + * Offers relevant actions for telephone numbers. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class TelResultHandler extends ResultHandler { + private static final int[] buttons = { + R.string.button_dial, + R.string.button_add_contact + }; + + public TelResultHandler(Activity activity, ParsedResult result) { + super(activity, result); + } + + @Override + public int getButtonCount() { + return buttons.length; + } + + @Override + public int getButtonText(int index) { + return buttons[index]; + } + + @Override + public void handleButtonPress(int index) { + TelParsedResult telResult = (TelParsedResult) getResult(); + switch (index) { + case 0: + dialPhoneFromUri(telResult.getTelURI()); + // When dialer comes up, it allows underlying display activity to continue or something, + // but app can't get camera in this state. Avoid issues by just quitting, only in the + // case of a phone number + getActivity().finish(); + break; + case 1: + String[] numbers = new String[1]; + numbers[0] = telResult.getNumber(); + addPhoneOnlyContact(numbers, null); + break; + } + } + + // Overriden so we can take advantage of Android's phone number hyphenation routines. + @Override + public CharSequence getDisplayContents() { + String contents = getResult().getDisplayResult(); + contents = contents.replace("\r", ""); + return PhoneNumberUtils.formatNumber(contents); + } + + @Override + public int getDisplayTitle() { + return R.string.result_tel; + } +} diff --git a/app/src/main/java/com/google/zxing/client/android/result/TextResultHandler.java b/app/src/main/java/com/google/zxing/client/android/result/TextResultHandler.java new file mode 100644 index 0000000..a5a9242 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/result/TextResultHandler.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.result; + +import android.app.Activity; + +import com.google.zxing.Result; +import com.google.zxing.client.result.ParsedResult; + +import net.foucry.pilldroid.R; + +/** + * This class handles TextParsedResult as well as unknown formats. It's the fallback handler. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class TextResultHandler extends ResultHandler { + + private static final int[] buttons = { + R.string.button_web_search, + R.string.button_share_by_email, + R.string.button_share_by_sms, + R.string.button_custom_product_search, + }; + + public TextResultHandler(Activity activity, ParsedResult result, Result rawResult) { + super(activity, result, rawResult); + } + + @Override + public int getButtonCount() { + return hasCustomProductSearch() ? buttons.length : buttons.length - 1; + } + + @Override + public int getButtonText(int index) { + return buttons[index]; + } + + @Override + public void handleButtonPress(int index) { + String text = getResult().getDisplayResult(); + switch (index) { + case 0: + webSearch(text); + break; + case 1: + shareByEmail(text); + break; + case 2: + shareBySMS(text); + break; + case 3: + openURL(fillInCustomSearchURL(text)); + break; + } + } + + @Override + public int getDisplayTitle() { + return R.string.result_text; + } +} diff --git a/app/src/main/java/com/google/zxing/client/android/result/URIResultHandler.java b/app/src/main/java/com/google/zxing/client/android/result/URIResultHandler.java new file mode 100644 index 0000000..2547462 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/result/URIResultHandler.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.result; + +import android.app.Activity; + +import com.google.zxing.client.android.LocaleManager; +import com.google.zxing.client.result.ParsedResult; +import com.google.zxing.client.result.URIParsedResult; + +import net.foucry.pilldroid.R; + +import java.util.Locale; + +/** + * Offers appropriate actions for URLS. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class URIResultHandler extends ResultHandler { + // URIs beginning with entries in this array will not be saved to history or copied to the + // clipboard for security. + private static final String[] SECURE_PROTOCOLS = { + "otpauth:" + }; + + private static final int[] buttons = { + R.string.button_open_browser, + R.string.button_share_by_email, + R.string.button_share_by_sms, + R.string.button_search_book_contents, + }; + + public URIResultHandler(Activity activity, ParsedResult result) { + super(activity, result); + } + + @Override + public int getButtonCount() { + if (LocaleManager.isBookSearchUrl(((URIParsedResult) getResult()).getURI())) { + return buttons.length; + } + return buttons.length - 1; + } + + @Override + public int getButtonText(int index) { + return buttons[index]; + } + + @Override + public Integer getDefaultButtonID() { + return 0; + } + + @Override + public void handleButtonPress(int index) { + URIParsedResult uriResult = (URIParsedResult) getResult(); + String uri = uriResult.getURI(); + switch (index) { + case 0: + openURL(uri); + break; + case 1: + shareByEmail(uri); + break; + case 2: + shareBySMS(uri); + break; + case 3: + searchBookContents(uri); + break; + } + } + + @Override + public int getDisplayTitle() { + return R.string.result_uri; + } + + @Override + public boolean areContentsSecure() { + URIParsedResult uriResult = (URIParsedResult) getResult(); + String uri = uriResult.getURI().toLowerCase(Locale.ENGLISH); + for (String secure : SECURE_PROTOCOLS) { + if (uri.startsWith(secure)) { + return true; + } + } + return false; + } +} diff --git a/app/src/main/java/com/google/zxing/client/android/result/WifiResultHandler.java b/app/src/main/java/com/google/zxing/client/android/result/WifiResultHandler.java new file mode 100644 index 0000000..28cdce5 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/result/WifiResultHandler.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.result; + +import android.app.Activity; +import android.content.Context; +import android.net.wifi.WifiManager; +import android.os.AsyncTask; +import android.util.Log; +import android.widget.Toast; + +import com.google.zxing.client.android.CaptureActivity; +import com.google.zxing.client.android.wifi.WifiConfigManager; +import com.google.zxing.client.result.ParsedResult; +import com.google.zxing.client.result.WifiParsedResult; + +import net.foucry.pilldroid.R; + +/** + * Handles wifi access information. + * + * @author Vikram Aggarwal + * @author Sean Owen + */ +public final class WifiResultHandler extends ResultHandler { + + private static final String TAG = WifiResultHandler.class.getSimpleName(); + + private final CaptureActivity parent; + + public WifiResultHandler(CaptureActivity activity, ParsedResult result) { + super(activity, result); + parent = activity; + } + + @Override + public int getButtonCount() { + // We just need one button, and that is to configure the wireless. This could change in the future. + return 1; + } + + @Override + public int getButtonText(int index) { + return R.string.button_wifi; + } + + @Override + public void handleButtonPress(int index) { + if (index == 0) { + WifiParsedResult wifiResult = (WifiParsedResult) getResult(); + WifiManager wifiManager = (WifiManager) getActivity().getSystemService(Context.WIFI_SERVICE); + if (wifiManager == null) { + Log.w(TAG, "No WifiManager available from device"); + return; + } + final Activity activity = getActivity(); + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(activity.getApplicationContext(), R.string.wifi_changing_network, Toast.LENGTH_SHORT).show(); + } + }); + new WifiConfigManager(wifiManager).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, wifiResult); + parent.restartPreviewAfterDelay(0L); + } + } + + // Display the name of the network and the network type to the user. + @Override + public CharSequence getDisplayContents() { + WifiParsedResult wifiResult = (WifiParsedResult) getResult(); + return wifiResult.getSsid() + " (" + wifiResult.getNetworkEncryption() + ')'; + } + + @Override + public int getDisplayTitle() { + return R.string.result_wifi; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/google/zxing/client/android/result/supplement/BookResultInfoRetriever.java b/app/src/main/java/com/google/zxing/client/android/result/supplement/BookResultInfoRetriever.java new file mode 100644 index 0000000..241abe2 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/result/supplement/BookResultInfoRetriever.java @@ -0,0 +1,107 @@ +/* + * Copyright 2011 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.result.supplement; + +import android.content.Context; +import android.widget.TextView; + +import com.google.zxing.client.android.HttpHelper; +import com.google.zxing.client.android.LocaleManager; +import com.google.zxing.client.android.history.HistoryManager; + +import net.foucry.pilldroid.R; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; + +/** + * @author Kamil Kaczmarczyk + * @author Sean Owen + */ +final class BookResultInfoRetriever extends SupplementalInfoRetriever { + + private final String isbn; + private final String source; + private final Context context; + + BookResultInfoRetriever(TextView textView, String isbn, HistoryManager historyManager, Context context) { + super(textView, historyManager); + this.isbn = isbn; + this.source = context.getString(R.string.msg_google_books); + this.context = context; + } + + @Override + void retrieveSupplementalInfo() throws IOException { + + CharSequence contents = HttpHelper.downloadViaHttp("https://www.googleapis.com/books/v1/volumes?q=isbn:" + isbn, + HttpHelper.ContentType.JSON); + + if (contents.length() == 0) { + return; + } + + String title; + String pages; + Collection authors = null; + + try { + + JSONObject topLevel = (JSONObject) new JSONTokener(contents.toString()).nextValue(); + JSONArray items = topLevel.optJSONArray("items"); + if (items == null || items.isNull(0)) { + return; + } + + JSONObject volumeInfo = ((JSONObject) items.get(0)).getJSONObject("volumeInfo"); + if (volumeInfo == null) { + return; + } + + title = volumeInfo.optString("title"); + pages = volumeInfo.optString("pageCount"); + + JSONArray authorsArray = volumeInfo.optJSONArray("authors"); + if (authorsArray != null && !authorsArray.isNull(0)) { + authors = new ArrayList<>(authorsArray.length()); + for (int i = 0; i < authorsArray.length(); i++) { + authors.add(authorsArray.getString(i)); + } + } + + } catch (JSONException e) { + throw new IOException(e); + } + + Collection newTexts = new ArrayList<>(); + maybeAddText(title, newTexts); + maybeAddTextSeries(authors, newTexts); + maybeAddText(pages == null || pages.isEmpty() ? null : pages + "pp.", newTexts); + + String baseBookUri = "http://www.google." + LocaleManager.getBookSearchCountryTLD(context) + + "/search?tbm=bks&source=zxing&q="; + + append(isbn, source, newTexts.toArray(new String[newTexts.size()]), baseBookUri + isbn); + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/result/supplement/ProductResultInfoRetriever.java b/app/src/main/java/com/google/zxing/client/android/result/supplement/ProductResultInfoRetriever.java new file mode 100644 index 0000000..84f3891 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/result/supplement/ProductResultInfoRetriever.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.result.supplement; + +import android.content.Context; +import android.text.Html; +import android.widget.TextView; + +import com.google.zxing.client.android.HttpHelper; +import com.google.zxing.client.android.LocaleManager; +import com.google.zxing.client.android.history.HistoryManager; + +import net.foucry.pilldroid.R; + +import java.io.IOException; +import java.net.URLEncoder; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + *

Retrieves product information from Google Product search.

+ * + *

Please do not reuse this code. Using results in this way requires permission + * from Google, and that is not granted to users via this project.

+ * + * @author Sean Owen + */ +final class ProductResultInfoRetriever extends SupplementalInfoRetriever { + + private static final Pattern[] PRODUCT_NAME_PRICE_PATTERNS = { + Pattern.compile(",event\\)\">([^<]+).+([^<]+)"), + Pattern.compile("owb63p\">([^<]+).+zdi3pb\">([^<]+)"), + }; + + private final String productID; + private final String source; + private final Context context; + + ProductResultInfoRetriever(TextView textView, String productID, HistoryManager historyManager, Context context) { + super(textView, historyManager); + this.productID = productID; + this.source = context.getString(R.string.msg_google_product); + this.context = context; + } + + @Override + void retrieveSupplementalInfo() throws IOException { + + String encodedProductID = URLEncoder.encode(productID, "UTF-8"); + String uri = "https://www.google." + LocaleManager.getProductSearchCountryTLD(context) + + "/m/products?ie=utf8&oe=utf8&scoring=p&source=zxing&q=" + encodedProductID; + CharSequence content = HttpHelper.downloadViaHttp(uri, HttpHelper.ContentType.HTML); + + for (Pattern p : PRODUCT_NAME_PRICE_PATTERNS) { + Matcher matcher = p.matcher(content); + if (matcher.find()) { + append(productID, + source, + new String[] { unescapeHTML(matcher.group(1)), unescapeHTML(matcher.group(2)) }, + uri); + break; + } + } + } + + private static String unescapeHTML(String raw) { + return Html.fromHtml(raw).toString(); + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/result/supplement/SupplementalInfoRetriever.java b/app/src/main/java/com/google/zxing/client/android/result/supplement/SupplementalInfoRetriever.java new file mode 100644 index 0000000..6aa8533 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/result/supplement/SupplementalInfoRetriever.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.result.supplement; + +import android.content.Context; +import android.os.AsyncTask; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.method.LinkMovementMethod; +import android.text.style.URLSpan; +import android.util.Log; +import android.widget.TextView; + +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.RejectedExecutionException; + +import com.google.zxing.client.android.history.HistoryManager; +import com.google.zxing.client.result.ISBNParsedResult; +import com.google.zxing.client.result.ParsedResult; +import com.google.zxing.client.result.ProductParsedResult; +import com.google.zxing.client.result.URIParsedResult; + +public abstract class SupplementalInfoRetriever extends AsyncTask { + + private static final String TAG = "SupplementalInfo"; + + public static void maybeInvokeRetrieval(TextView textView, + ParsedResult result, + HistoryManager historyManager, + Context context) { + try { + if (result instanceof URIParsedResult) { + SupplementalInfoRetriever uriRetriever = + new URIResultInfoRetriever(textView, (URIParsedResult) result, historyManager, context); + uriRetriever.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + SupplementalInfoRetriever titleRetriever = + new TitleRetriever(textView, (URIParsedResult) result, historyManager); + titleRetriever.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else if (result instanceof ProductParsedResult) { + ProductParsedResult productParsedResult = (ProductParsedResult) result; + String productID = productParsedResult.getProductID(); + SupplementalInfoRetriever productRetriever = + new ProductResultInfoRetriever(textView, productID, historyManager, context); + productRetriever.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else if (result instanceof ISBNParsedResult) { + String isbn = ((ISBNParsedResult) result).getISBN(); + SupplementalInfoRetriever productInfoRetriever = + new ProductResultInfoRetriever(textView, isbn, historyManager, context); + productInfoRetriever.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + SupplementalInfoRetriever bookInfoRetriever = + new BookResultInfoRetriever(textView, isbn, historyManager, context); + bookInfoRetriever.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + } catch (RejectedExecutionException ree) { + // do nothing + } + } + + private final WeakReference textViewRef; + private final WeakReference historyManagerRef; + private final Collection newContents; + private final Collection newHistories; + + SupplementalInfoRetriever(TextView textView, HistoryManager historyManager) { + textViewRef = new WeakReference<>(textView); + historyManagerRef = new WeakReference<>(historyManager); + newContents = new ArrayList<>(); + newHistories = new ArrayList<>(); + } + + @Override + public final Object doInBackground(Object... args) { + try { + retrieveSupplementalInfo(); + } catch (IOException e) { + Log.w(TAG, e); + } + return null; + } + + @Override + protected final void onPostExecute(Object arg) { + TextView textView = textViewRef.get(); + if (textView != null) { + for (CharSequence content : newContents) { + textView.append(content); + } + textView.setMovementMethod(LinkMovementMethod.getInstance()); + } + HistoryManager historyManager = historyManagerRef.get(); + if (historyManager != null) { + for (String[] text : newHistories) { + historyManager.addHistoryItemDetails(text[0], text[1]); + } + } + } + + abstract void retrieveSupplementalInfo() throws IOException; + + final void append(String itemID, String source, String[] newTexts, String linkURL) { + + StringBuilder newTextCombined = new StringBuilder(); + + if (source != null) { + newTextCombined.append(source).append(' '); + } + + int linkStart = newTextCombined.length(); + + boolean first = true; + for (String newText : newTexts) { + if (first) { + newTextCombined.append(newText); + first = false; + } else { + newTextCombined.append(" ["); + newTextCombined.append(newText); + newTextCombined.append(']'); + } + } + + int linkEnd = newTextCombined.length(); + + String newText = newTextCombined.toString(); + Spannable content = new SpannableString(newText + "\n\n"); + if (linkURL != null) { + // Strangely, some Android browsers don't seem to register to handle HTTP:// or HTTPS://. + // Lower-case these as it should always be OK to lower-case these schemes. + if (linkURL.startsWith("HTTP://")) { + linkURL = "http" + linkURL.substring(4); + } else if (linkURL.startsWith("HTTPS://")) { + linkURL = "https" + linkURL.substring(5); + } + content.setSpan(new URLSpan(linkURL), linkStart, linkEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + newContents.add(content); + newHistories.add(new String[] {itemID, newText}); + } + + static void maybeAddText(String text, Collection texts) { + if (text != null && !text.isEmpty()) { + texts.add(text); + } + } + + static void maybeAddTextSeries(Collection textSeries, Collection texts) { + if (textSeries != null && !textSeries.isEmpty()) { + boolean first = true; + StringBuilder authorsText = new StringBuilder(); + for (String author : textSeries) { + if (first) { + first = false; + } else { + authorsText.append(", "); + } + authorsText.append(author); + } + texts.add(authorsText.toString()); + } + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/result/supplement/TitleRetriever.java b/app/src/main/java/com/google/zxing/client/android/result/supplement/TitleRetriever.java new file mode 100644 index 0000000..93467fc --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/result/supplement/TitleRetriever.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.result.supplement; + +import android.text.Html; +import android.widget.TextView; +import com.google.zxing.client.android.HttpHelper; +import com.google.zxing.client.android.history.HistoryManager; +import com.google.zxing.client.result.URIParsedResult; + +import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Retrieves the title of a web page as supplemental info. + * + * @author Sean Owen + */ +final class TitleRetriever extends SupplementalInfoRetriever { + + private static final Pattern TITLE_PATTERN = Pattern.compile("([^<]+)"); + private static final int MAX_TITLE_LEN = 100; + + private final String httpUrl; + + TitleRetriever(TextView textView, URIParsedResult result, HistoryManager historyManager) { + super(textView, historyManager); + this.httpUrl = result.getURI(); + } + + @Override + void retrieveSupplementalInfo() { + CharSequence contents; + try { + contents = HttpHelper.downloadViaHttp(httpUrl, HttpHelper.ContentType.HTML, 4096); + } catch (IOException ioe) { + // ignore this + return; + } + if (contents != null && contents.length() > 0) { + Matcher m = TITLE_PATTERN.matcher(contents); + if (m.find()) { + String title = m.group(1); + if (title != null && !title.isEmpty()) { + title = Html.fromHtml(title).toString(); + if (title.length() > MAX_TITLE_LEN) { + title = title.substring(0, MAX_TITLE_LEN) + "..."; + } + append(httpUrl, null, new String[] {title}, httpUrl); + } + } + } + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/result/supplement/URIResultInfoRetriever.java b/app/src/main/java/com/google/zxing/client/android/result/supplement/URIResultInfoRetriever.java new file mode 100644 index 0000000..0f6df6f --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/result/supplement/URIResultInfoRetriever.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.result.supplement; + +import android.content.Context; +import android.widget.TextView; + +import com.google.zxing.client.android.HttpHelper; +import com.google.zxing.client.android.history.HistoryManager; +import com.google.zxing.client.result.URIParsedResult; + +import net.foucry.pilldroid.R; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; + +final class URIResultInfoRetriever extends SupplementalInfoRetriever { + + private static final int MAX_REDIRECTS = 5; + + private final URIParsedResult result; + private final String redirectString; + + URIResultInfoRetriever(TextView textView, URIParsedResult result, HistoryManager historyManager, Context context) { + super(textView, historyManager); + redirectString = context.getString(R.string.msg_redirect); + this.result = result; + } + + @Override + void retrieveSupplementalInfo() throws IOException { + URI oldURI; + try { + oldURI = new URI(result.getURI()); + } catch (URISyntaxException ignored) { + return; + } + URI newURI = HttpHelper.unredirect(oldURI); + int count = 0; + while (count++ < MAX_REDIRECTS && !oldURI.equals(newURI)) { + append(result.getDisplayResult(), + null, + new String[] { redirectString + " : " + newURI }, + newURI.toString()); + oldURI = newURI; + newURI = HttpHelper.unredirect(newURI); + } + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/share/AppInfo.java b/app/src/main/java/com/google/zxing/client/android/share/AppInfo.java new file mode 100644 index 0000000..94bde9b --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/share/AppInfo.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2013 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.share; + +import android.graphics.drawable.Drawable; + +final class AppInfo implements Comparable<AppInfo> { + + private final String packageName; + private final String label; + private final Drawable icon; + + AppInfo(String packageName, String label, Drawable icon) { + this.packageName = packageName; + this.label = label; + this.icon = icon; + } + + String getPackageName() { + return packageName; + } + + Drawable getIcon() { + return icon; + } + + @Override + public String toString() { + return label; + } + + @Override + public int compareTo(AppInfo another) { + return label.compareTo(another.label); + } + + @Override + public int hashCode() { + return label.hashCode(); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof AppInfo)) { + return false; + } + AppInfo another = (AppInfo) other; + return label.equals(another.label); + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/share/AppPickerActivity.java b/app/src/main/java/com/google/zxing/client/android/share/AppPickerActivity.java new file mode 100644 index 0000000..778beaf --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/share/AppPickerActivity.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.share; + +import android.app.ListActivity; +import android.content.Intent; +import android.os.AsyncTask; +import android.view.View; +import android.widget.Adapter; +import android.widget.ListView; + +import java.util.List; + +public final class AppPickerActivity extends ListActivity { + + private AsyncTask<Object,Object,List<AppInfo>> backgroundTask; + + @Override + protected void onResume() { + super.onResume(); + backgroundTask = new LoadPackagesAsyncTask(this); + backgroundTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + @Override + protected void onPause() { + AsyncTask<?,?,?> task = backgroundTask; + if (task != null) { + task.cancel(true); + backgroundTask = null; + } + super.onPause(); + } + + @Override + protected void onListItemClick(ListView l, View view, int position, long id) { + Adapter adapter = getListAdapter(); + if (position >= 0 && position < adapter.getCount()) { + String packageName = ((AppInfo) adapter.getItem(position)).getPackageName(); + Intent intent = new Intent(); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + intent.putExtra("url", "market://details?id=" + packageName); // Browser.BookmarkColumns.URL + setResult(RESULT_OK, intent); + } else { + setResult(RESULT_CANCELED); + } + finish(); + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/share/BookmarkAdapter.java b/app/src/main/java/com/google/zxing/client/android/share/BookmarkAdapter.java new file mode 100644 index 0000000..b95a6ee --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/share/BookmarkAdapter.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2011 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.share; + +import android.content.Context; +import android.database.Cursor; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.LinearLayout; +import android.widget.TextView; + +import net.foucry.pilldroid.R; + +/** + * A custom adapter designed to fetch bookmarks from a cursor. Before Honeycomb we used + * SimpleCursorAdapter, but it assumes the existence of an _id column, and the bookmark schema was + * rewritten for HC without one. This caused the app to crash, hence this new class, which is + * forwards and backwards compatible. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +final class BookmarkAdapter extends BaseAdapter { + private final Context context; + private final Cursor cursor; + + BookmarkAdapter(Context context, Cursor cursor) { + this.context = context; + this.cursor = cursor; + } + + @Override + public int getCount() { + return cursor.isClosed() ? 0 : cursor.getCount(); + } + + @Override + public Object getItem(int index) { + // Not used, so no point in retrieving it. + return null; + } + + @Override + public long getItemId(int index) { + return index; + } + + @Override + public View getView(int index, View view, ViewGroup viewGroup) { + View layout; + if (view instanceof LinearLayout) { + layout = view; + } else { + LayoutInflater factory = LayoutInflater.from(context); + layout = factory.inflate(R.layout.bookmark_picker_list_item, viewGroup, false); + } + + if (!cursor.isClosed()) { + cursor.moveToPosition(index); + CharSequence title = cursor.getString(BookmarkPickerActivity.TITLE_COLUMN); + ((TextView) layout.findViewById(R.id.bookmark_title)).setText(title); + CharSequence url = cursor.getString(BookmarkPickerActivity.URL_COLUMN); + ((TextView) layout.findViewById(R.id.bookmark_url)).setText(url); + } // Otherwise... just don't update as the object is shutting down + return layout; + } +} diff --git a/app/src/main/java/com/google/zxing/client/android/share/BookmarkPickerActivity.java b/app/src/main/java/com/google/zxing/client/android/share/BookmarkPickerActivity.java new file mode 100644 index 0000000..c4cc1f3 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/share/BookmarkPickerActivity.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.share; + +import android.app.ListActivity; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.util.Log; +import android.view.View; +import android.widget.ListView; + +/** + * This class is only needed because I can't successfully send an ACTION_PICK intent to + * com.android.browser.BrowserBookmarksPage. It can go away if that starts working in the future. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class BookmarkPickerActivity extends ListActivity { + + private static final String TAG = BookmarkPickerActivity.class.getSimpleName(); + + private static final String[] BOOKMARK_PROJECTION = { + "title", // Browser.BookmarkColumns.TITLE + "url", // Browser.BookmarkColumns.URL + }; + // Copied from android.provider.Browser.BOOKMARKS_URI: + private static final Uri BOOKMARKS_URI = Uri.parse("content://browser/bookmarks"); + + static final int TITLE_COLUMN = 0; + static final int URL_COLUMN = 1; + + private static final String BOOKMARK_SELECTION = "bookmark = 1 AND url IS NOT NULL"; + + private Cursor cursor; + + @Override + protected void onResume() { + super.onResume(); + cursor = getContentResolver().query(BOOKMARKS_URI, BOOKMARK_PROJECTION, + BOOKMARK_SELECTION, null, null); + if (cursor == null) { + Log.w(TAG, "No cursor returned for bookmark query"); + finish(); + return; + } + setListAdapter(new BookmarkAdapter(this, cursor)); + } + + @Override + protected void onPause() { + if (cursor != null) { + cursor.close(); + cursor = null; + } + super.onPause(); + } + + @Override + protected void onListItemClick(ListView l, View view, int position, long id) { + if (!cursor.isClosed() && cursor.moveToPosition(position)) { + Intent intent = new Intent(); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + intent.putExtra("title", cursor.getString(TITLE_COLUMN)); // Browser.BookmarkColumns.TITLE + intent.putExtra("url", cursor.getString(URL_COLUMN)); // Browser.BookmarkColumns.URL + setResult(RESULT_OK, intent); + } else { + setResult(RESULT_CANCELED); + } + finish(); + } +} diff --git a/app/src/main/java/com/google/zxing/client/android/share/LoadPackagesAsyncTask.java b/app/src/main/java/com/google/zxing/client/android/share/LoadPackagesAsyncTask.java new file mode 100644 index 0000000..a49f2a0 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/share/LoadPackagesAsyncTask.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.share; + +import android.app.ListActivity; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageItemInfo; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.ListAdapter; +import net.foucry.pilldroid.R; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Loads a list of packages installed on the device asynchronously. + * + * @author Sean Owen + */ +final class LoadPackagesAsyncTask extends AsyncTask<Object,Object,List<AppInfo>> { + + private static final String[] PKG_PREFIX_WHITELIST = { + "com.google.android.apps.", + }; + private static final String[] PKG_PREFIX_BLACKLIST = { + "com.android.", + "android", + "com.google.android.", + "com.htc", + }; + + private final ListActivity activity; + + LoadPackagesAsyncTask(ListActivity activity) { + this.activity = activity; + } + + @Override + protected List<AppInfo> doInBackground(Object... objects) { + List<AppInfo> labelsPackages = new ArrayList<>(); + PackageManager packageManager = activity.getPackageManager(); + Iterable<ApplicationInfo> appInfos = packageManager.getInstalledApplications(0); + for (PackageItemInfo appInfo : appInfos) { + String packageName = appInfo.packageName; + if (!isHidden(packageName)) { + CharSequence label = appInfo.loadLabel(packageManager); + Drawable icon = appInfo.loadIcon(packageManager); + if (label != null) { + labelsPackages.add(new AppInfo(packageName, label.toString(), icon)); + } + } + } + Collections.sort(labelsPackages); + return labelsPackages; + } + + private static boolean isHidden(String packageName) { + if (packageName == null) { + return true; + } + for (String prefix : PKG_PREFIX_WHITELIST) { + if (packageName.startsWith(prefix)) { + return false; + } + } + for (String prefix : PKG_PREFIX_BLACKLIST) { + if (packageName.startsWith(prefix)) { + return true; + } + } + return false; + } + + @Override + protected void onPostExecute(final List<AppInfo> results) { + ListAdapter listAdapter = new ArrayAdapter<AppInfo>(activity, + R.layout.app_picker_list_item, + R.id.app_picker_list_item_label, + results) { + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view = super.getView(position, convertView, parent); + Drawable icon = results.get(position).getIcon(); + if (icon != null) { + ((ImageView) view.findViewById(R.id.app_picker_list_item_icon)).setImageDrawable(icon); + } + return view; + } + }; + activity.setListAdapter(listAdapter); + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/share/ShareActivity.java b/app/src/main/java/com/google/zxing/client/android/share/ShareActivity.java new file mode 100644 index 0000000..d779d20 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/share/ShareActivity.java @@ -0,0 +1,306 @@ +/* + * Copyright (C) 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.share; + +import android.app.Activity; +import android.content.ContentResolver; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.provider.BaseColumns; +import android.provider.ContactsContract; +import android.util.Log; +import android.view.KeyEvent; +import android.view.View; +import android.widget.TextView; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.client.android.Contents; +import com.google.zxing.client.android.Intents; +import com.google.zxing.client.android.clipboard.ClipboardInterface; + +import net.foucry.pilldroid.R; + +/** + * Barcode Scanner can share data like contacts and bookmarks by displaying a QR Code on screen, + * such that another user can scan the barcode with their phone. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class ShareActivity extends Activity { + + private static final String TAG = ShareActivity.class.getSimpleName(); + + private static final int PICK_BOOKMARK = 0; + private static final int PICK_CONTACT = 1; + private static final int PICK_APP = 2; + + private View clipboardButton; + + private final View.OnClickListener contactListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + startActivityForResult(intent, PICK_CONTACT); + } + }; + + private final View.OnClickListener bookmarkListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(Intent.ACTION_PICK); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + intent.setClassName(ShareActivity.this, BookmarkPickerActivity.class.getName()); + startActivityForResult(intent, PICK_BOOKMARK); + } + }; + + private final View.OnClickListener appListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(Intent.ACTION_PICK); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + intent.setClassName(ShareActivity.this, AppPickerActivity.class.getName()); + startActivityForResult(intent, PICK_APP); + } + }; + + private final View.OnClickListener clipboardListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + // Should always be true, because we grey out the clipboard button in onResume() if it's empty + CharSequence text = ClipboardInterface.getText(ShareActivity.this); + if (text != null) { + launchSearch(text.toString()); + } + } + }; + + private final View.OnKeyListener textListener = new View.OnKeyListener() { + @Override + public boolean onKey(View view, int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_DOWN) { + String text = ((TextView) view).getText().toString(); + if (text != null && !text.isEmpty()) { + launchSearch(text); + } + return true; + } + return false; + } + }; + + private void launchSearch(String text) { + Intent intent = new Intent(Intents.Encode.ACTION); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + intent.putExtra(Intents.Encode.TYPE, Contents.Type.TEXT); + intent.putExtra(Intents.Encode.DATA, text); + intent.putExtra(Intents.Encode.FORMAT, BarcodeFormat.QR_CODE.toString()); + startActivity(intent); + } + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + setContentView(R.layout.share); + + findViewById(R.id.share_contact_button).setOnClickListener(contactListener); + if (Build.VERSION.SDK_INT >= 23) { // Marshmallow / 6.0 + // Can't access bookmarks in 6.0+ + findViewById(R.id.share_bookmark_button).setEnabled(false); + } else { + findViewById(R.id.share_bookmark_button).setOnClickListener(bookmarkListener); + } + findViewById(R.id.share_app_button).setOnClickListener(appListener); + clipboardButton = findViewById(R.id.share_clipboard_button); + clipboardButton.setOnClickListener(clipboardListener); + findViewById(R.id.share_text_view).setOnKeyListener(textListener); + } + + @Override + protected void onResume() { + super.onResume(); + clipboardButton.setEnabled(ClipboardInterface.hasText(this)); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent intent) { + if (resultCode == RESULT_OK) { + switch (requestCode) { + case PICK_BOOKMARK: + case PICK_APP: + showTextAsBarcode(intent.getStringExtra("url")); // Browser.BookmarkColumns.URL + break; + case PICK_CONTACT: + // Data field is content://contacts/people/984 + showContactAsBarcode(intent.getData()); + break; + } + } + } + + private void showTextAsBarcode(String text) { + Log.i(TAG, "Showing text as barcode: " + text); + if (text == null) { + return; // Show error? + } + Intent intent = new Intent(Intents.Encode.ACTION); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + intent.putExtra(Intents.Encode.TYPE, Contents.Type.TEXT); + intent.putExtra(Intents.Encode.DATA, text); + intent.putExtra(Intents.Encode.FORMAT, BarcodeFormat.QR_CODE.toString()); + startActivity(intent); + } + + /** + * Takes a contact Uri and does the necessary database lookups to retrieve that person's info, + * then sends an Encode intent to render it as a QR Code. + * + * @param contactUri A Uri of the form content://contacts/people/17 + */ + private void showContactAsBarcode(Uri contactUri) { + Log.i(TAG, "Showing contact URI as barcode: " + contactUri); + if (contactUri == null) { + return; // Show error? + } + ContentResolver resolver = getContentResolver(); + + Cursor cursor; + try { + // We're seeing about six reports a week of this exception although I don't understand why. + cursor = resolver.query(contactUri, null, null, null, null); + } catch (IllegalArgumentException ignored) { + return; + } + if (cursor == null) { + return; + } + + String id; + String name; + boolean hasPhone; + try { + if (!cursor.moveToFirst()) { + return; + } + + id = cursor.getString(cursor.getColumnIndex(BaseColumns._ID)); + name = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)); + hasPhone = cursor.getInt(cursor.getColumnIndex(ContactsContract.Contacts.HAS_PHONE_NUMBER)) > 0; + + + } finally { + cursor.close(); + } + + // Don't require a name to be present, this contact might be just a phone number. + Bundle bundle = new Bundle(); + if (name != null && !name.isEmpty()) { + bundle.putString(ContactsContract.Intents.Insert.NAME, massageContactData(name)); + } + + if (hasPhone) { + Cursor phonesCursor = resolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, + null, + ContactsContract.CommonDataKinds.Phone.CONTACT_ID + '=' + id, + null, + null); + if (phonesCursor != null) { + try { + int foundPhone = 0; + int phonesNumberColumn = phonesCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER); + int phoneTypeColumn = phonesCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.TYPE); + while (phonesCursor.moveToNext() && foundPhone < Contents.PHONE_KEYS.length) { + String number = phonesCursor.getString(phonesNumberColumn); + if (number != null && !number.isEmpty()) { + bundle.putString(Contents.PHONE_KEYS[foundPhone], massageContactData(number)); + } + int type = phonesCursor.getInt(phoneTypeColumn); + bundle.putInt(Contents.PHONE_TYPE_KEYS[foundPhone], type); + foundPhone++; + } + } finally { + phonesCursor.close(); + } + } + } + + Cursor methodsCursor = resolver.query(ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_URI, + null, + ContactsContract.CommonDataKinds.StructuredPostal.CONTACT_ID + '=' + id, + null, + null); + if (methodsCursor != null) { + try { + if (methodsCursor.moveToNext()) { + String data = methodsCursor.getString( + methodsCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS)); + if (data != null && !data.isEmpty()) { + bundle.putString(ContactsContract.Intents.Insert.POSTAL, massageContactData(data)); + } + } + } finally { + methodsCursor.close(); + } + } + + Cursor emailCursor = resolver.query(ContactsContract.CommonDataKinds.Email.CONTENT_URI, + null, + ContactsContract.CommonDataKinds.Email.CONTACT_ID + '=' + id, + null, + null); + if (emailCursor != null) { + try { + int foundEmail = 0; + int emailColumn = emailCursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.DATA); + while (emailCursor.moveToNext() && foundEmail < Contents.EMAIL_KEYS.length) { + String email = emailCursor.getString(emailColumn); + if (email != null && !email.isEmpty()) { + bundle.putString(Contents.EMAIL_KEYS[foundEmail], massageContactData(email)); + } + foundEmail++; + } + } finally { + emailCursor.close(); + } + } + + Intent intent = new Intent(Intents.Encode.ACTION); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + intent.putExtra(Intents.Encode.TYPE, Contents.Type.CONTACT); + intent.putExtra(Intents.Encode.DATA, bundle); + intent.putExtra(Intents.Encode.FORMAT, BarcodeFormat.QR_CODE.toString()); + + Log.i(TAG, "Sending bundle for encoding: " + bundle); + startActivity(intent); + } + + private static String massageContactData(String data) { + // For now -- make sure we don't put newlines in shared contact data. It messes up + // any known encoding of contact data. Replace with space. + if (data.indexOf('\n') >= 0) { + data = data.replace("\n", " "); + } + if (data.indexOf('\r') >= 0) { + data = data.replace("\r", " "); + } + return data; + } +} diff --git a/app/src/main/java/com/google/zxing/client/android/wifi/NetworkType.java b/app/src/main/java/com/google/zxing/client/android/wifi/NetworkType.java new file mode 100644 index 0000000..809e3c6 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/wifi/NetworkType.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2011 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.wifi; + +enum NetworkType { + + WEP, + WPA, + NO_PASSWORD; + + static NetworkType forIntentValue(String networkTypeString) { + if (networkTypeString == null) { + return NO_PASSWORD; + } + if ("WPA".equals(networkTypeString)) { + return WPA; + } + if ("WEP".equals(networkTypeString)) { + return WEP; + } + if ("nopass".equals(networkTypeString)) { + return NO_PASSWORD; + } + throw new IllegalArgumentException(networkTypeString); + } + +} diff --git a/app/src/main/java/com/google/zxing/client/android/wifi/WifiConfigManager.java b/app/src/main/java/com/google/zxing/client/android/wifi/WifiConfigManager.java new file mode 100644 index 0000000..ba11f90 --- /dev/null +++ b/app/src/main/java/com/google/zxing/client/android/wifi/WifiConfigManager.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2011 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android.wifi; + +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiManager; +import android.os.AsyncTask; +import android.util.Log; + +import java.util.regex.Pattern; + +import com.google.zxing.client.result.WifiParsedResult; + +/** + * @author Vikram Aggarwal + * @author Sean Owen + */ +public final class WifiConfigManager extends AsyncTask<WifiParsedResult,Object,Object> { + + private static final String TAG = WifiConfigManager.class.getSimpleName(); + + private static final Pattern HEX_DIGITS = Pattern.compile("[0-9A-Fa-f]+"); + + private final WifiManager wifiManager; + + public WifiConfigManager(WifiManager wifiManager) { + this.wifiManager = wifiManager; + } + + @Override + protected Object doInBackground(WifiParsedResult... args) { + WifiParsedResult theWifiResult = args[0]; + // Start WiFi, otherwise nothing will work + if (!wifiManager.isWifiEnabled()) { + Log.i(TAG, "Enabling wi-fi..."); + if (wifiManager.setWifiEnabled(true)) { + Log.i(TAG, "Wi-fi enabled"); + } else { + Log.w(TAG, "Wi-fi could not be enabled!"); + return null; + } + // This happens very quickly, but need to wait for it to enable. A little busy wait? + int count = 0; + while (!wifiManager.isWifiEnabled()) { + if (count >= 10) { + Log.i(TAG, "Took too long to enable wi-fi, quitting"); + return null; + } + Log.i(TAG, "Still waiting for wi-fi to enable..."); + try { + Thread.sleep(1000L); + } catch (InterruptedException ie) { + // continue + } + count++; + } + } + String networkTypeString = theWifiResult.getNetworkEncryption(); + NetworkType networkType; + try { + networkType = NetworkType.forIntentValue(networkTypeString); + } catch (IllegalArgumentException ignored) { + Log.w(TAG, "Bad network type; see NetworkType values: " + networkTypeString); + return null; + } + if (networkType == NetworkType.NO_PASSWORD) { + changeNetworkUnEncrypted(wifiManager, theWifiResult); + } else { + String password = theWifiResult.getPassword(); + if (password != null && !password.isEmpty()) { + if (networkType == NetworkType.WEP) { + changeNetworkWEP(wifiManager, theWifiResult); + } else if (networkType == NetworkType.WPA) { + changeNetworkWPA(wifiManager, theWifiResult); + } + } + } + return null; + } + + /** + * Update the network: either create a new network or modify an existing network + * @param config the new network configuration + */ + private static void updateNetwork(WifiManager wifiManager, WifiConfiguration config) { + Integer foundNetworkID = findNetworkInExistingConfig(wifiManager, config.SSID); + if (foundNetworkID != null) { + Log.i(TAG, "Removing old configuration for network " + config.SSID); + wifiManager.removeNetwork(foundNetworkID); + wifiManager.saveConfiguration(); + } + int networkId = wifiManager.addNetwork(config); + if (networkId >= 0) { + // Try to disable the current network and start a new one. + if (wifiManager.enableNetwork(networkId, true)) { + Log.i(TAG, "Associating to network " + config.SSID); + wifiManager.saveConfiguration(); + } else { + Log.w(TAG, "Failed to enable network " + config.SSID); + } + } else { + Log.w(TAG, "Unable to add network " + config.SSID); + } + } + + private static WifiConfiguration changeNetworkCommon(WifiParsedResult wifiResult) { + WifiConfiguration config = new WifiConfiguration(); + config.allowedAuthAlgorithms.clear(); + config.allowedGroupCiphers.clear(); + config.allowedKeyManagement.clear(); + config.allowedPairwiseCiphers.clear(); + config.allowedProtocols.clear(); + // Android API insists that an ascii SSID must be quoted to be correctly handled. + config.SSID = quoteNonHex(wifiResult.getSsid()); + config.hiddenSSID = wifiResult.isHidden(); + return config; + } + + // Adding a WEP network + private static void changeNetworkWEP(WifiManager wifiManager, WifiParsedResult wifiResult) { + WifiConfiguration config = changeNetworkCommon(wifiResult); + config.wepKeys[0] = quoteNonHex(wifiResult.getPassword(), 10, 26, 58); + config.wepTxKeyIndex = 0; + config.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.SHARED); + config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE); + config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP); + config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP); + config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40); + config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104); + updateNetwork(wifiManager, config); + } + + // Adding a WPA or WPA2 network + private static void changeNetworkWPA(WifiManager wifiManager, WifiParsedResult wifiResult) { + WifiConfiguration config = changeNetworkCommon(wifiResult); + // Hex passwords that are 64 bits long are not to be quoted. + config.preSharedKey = quoteNonHex(wifiResult.getPassword(), 64); + config.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN); + config.allowedProtocols.set(WifiConfiguration.Protocol.WPA); // For WPA + config.allowedProtocols.set(WifiConfiguration.Protocol.RSN); // For WPA2 + config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK); + config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_EAP); + config.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP); + config.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP); + config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP); + config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP); + updateNetwork(wifiManager, config); + } + + // Adding an open, unsecured network + private static void changeNetworkUnEncrypted(WifiManager wifiManager, WifiParsedResult wifiResult) { + WifiConfiguration config = changeNetworkCommon(wifiResult); + config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE); + updateNetwork(wifiManager, config); + } + + private static Integer findNetworkInExistingConfig(WifiManager wifiManager, String ssid) { + Iterable<WifiConfiguration> existingConfigs = wifiManager.getConfiguredNetworks(); + if (existingConfigs != null) { + for (WifiConfiguration existingConfig : existingConfigs) { + String existingSSID = existingConfig.SSID; + if (existingSSID != null && existingSSID.equals(ssid)) { + return existingConfig.networkId; + } + } + } + return null; + } + + private static String quoteNonHex(String value, int... allowedLengths) { + return isHexOfLength(value, allowedLengths) ? value : convertToQuotedString(value); + } + + /** + * Encloses the incoming string inside double quotes, if it isn't already quoted. + * @param s the input string + * @return a quoted string, of the form "input". If the input string is null, it returns null + * as well. + */ + private static String convertToQuotedString(String s) { + if (s == null || s.isEmpty()) { + return null; + } + // If already quoted, return as-is + if (s.charAt(0) == '"' && s.charAt(s.length() - 1) == '"') { + return s; + } + return '\"' + s + '\"'; + } + + /** + * @param value input to check + * @param allowedLengths allowed lengths, if any + * @return true if value is a non-null, non-empty string of hex digits, and if allowed lengths are given, has + * an allowed length + */ + private static boolean isHexOfLength(CharSequence value, int... allowedLengths) { + if (value == null || !HEX_DIGITS.matcher(value).matches()) { + return false; + } + if (allowedLengths.length == 0) { + return true; + } + for (int length : allowedLengths) { + if (value.length() == length) { + return true; + } + } + return false; + } + +} diff --git a/app/src/main/java/net/foucry/pilldroid/MedicamentListActivity.java b/app/src/main/java/net/foucry/pilldroid/MedicamentListActivity.java index 5d357fc..5bfe26a 100644 --- a/app/src/main/java/net/foucry/pilldroid/MedicamentListActivity.java +++ b/app/src/main/java/net/foucry/pilldroid/MedicamentListActivity.java @@ -4,12 +4,10 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.support.annotation.NonNull; +import android.support.design.widget.FloatingActionButton; import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; -import android.support.design.widget.FloatingActionButton; -import android.support.design.widget.Snackbar; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -18,11 +16,6 @@ import android.widget.ImageView; import android.widget.SimpleCursorAdapter; import android.widget.TextView; - -import net.foucry.pilldroid.Medicament; -import net.foucry.pilldroid.dummy.DummyContent; -import static net.foucry.pilldroid.UtilDate.*; -import static net.foucry.pilldroid.Utils.*; import java.text.SimpleDateFormat; import java.util.Collections; import java.util.Comparator; @@ -30,6 +23,9 @@ import java.util.List; import java.util.Locale; import java.util.Random; +import static net.foucry.pilldroid.UtilDate.date2String; +import static net.foucry.pilldroid.Utils.doubleRandomInclusive; + /** * An activity representing a list of Medicaments. This activity * has different presentations for handset and tablet-size devices. On @@ -74,8 +70,12 @@ public class MedicamentListActivity extends AppCompatActivity { fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - Snackbar.make(view, "Will be used to add a drug to the list", Snackbar.LENGTH_LONG) - .setAction("Action", null).show(); + /* Snackbar.make(view, "Will be used to add a drug to the list", Snackbar.LENGTH_LONG) + .setAction("Action", null).show(); */ + Intent intent = new Intent("com.google.zxing.client.android.SCAN"); + intent.putExtra("SCAN_MODE", "CODE_128"); + //intent.putExtra("SCAN_FORMATS", "EAN_13,DATA_MATRIX"); + startActivityForResult(intent, 0); } }); @@ -247,6 +247,27 @@ public class MedicamentListActivity extends AppCompatActivity { public String toString() { return super.toString() + " '" + mContentView.getText() + "'"; } + + public void scanNow(View view) { + Intent intent = new Intent("com.google.zxing.client.android.SCAN"); + intent.putExtra("SCAN_MODE", "CODE_128"); + //intent.putExtra("SCAN_FORMATS", "EAN_13,DATA_MATRIX"); + startActivityForResult(intent, 0); + } + + public void onActivityResult(int requestCode, int resultCode, Intent intent) { + if (requestCode == 0) { + if (resultCode == RESULT_OK) { + String contents = intent.getStringExtra("SCAN_RESULT"); + String format = intent.getStringExtra("SCAN_RESULT_FORMAT"); + Log.i("Prout", format); + Log.i("Prout", contents); + // Handle successful scan + } else if (resultCode == RESULT_CANCELED) { + // Handle cancel + } + } + } } } } diff --git a/app/src/main/res/drawable/share_via_barcode.png b/app/src/main/res/drawable/share_via_barcode.png new file mode 100644 index 0000000..56c3449 Binary files /dev/null and b/app/src/main/res/drawable/share_via_barcode.png differ diff --git a/app/src/main/res/layout/activity_medicament_list.xml b/app/src/main/res/layout/activity_medicament_list.xml index a7aa1fb..33ff41a 100644 --- a/app/src/main/res/layout/activity_medicament_list.xml +++ b/app/src/main/res/layout/activity_medicament_list.xml @@ -36,6 +36,7 @@ android:layout_height="60dp" android:layout_gravity="bottom|end" android:layout_margin="@dimen/fab_margin" + android:onClick="scanNow" android:src="@android:drawable/ic_input_add" android:adjustViewBounds="true" android:clickable="false" diff --git a/app/src/main/res/layout/app_picker_list_item.xml b/app/src/main/res/layout/app_picker_list_item.xml new file mode 100644 index 0000000..0f68618 --- /dev/null +++ b/app/src/main/res/layout/app_picker_list_item.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2008 ZXing authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:orientation="horizontal" + android:layout_width="fill_parent" + android:layout_height="wrap_content"> + + <ImageView android:id="@+id/app_picker_list_item_icon" + android:layout_width="64dip" + android:layout_height="64dip" + android:scaleType="centerInside" + android:padding="@dimen/half_padding" + tools:ignore="ContentDescription"/> + + <TextView android:id="@+id/app_picker_list_item_label" + android:layout_width="wrap_content" + android:layout_height="fill_parent" + android:gravity="center_vertical" + android:textAppearance="?android:attr/textAppearanceLarge" + android:singleLine="true" + android:padding="@dimen/half_padding"/> + +</LinearLayout> diff --git a/app/src/main/res/layout/bookmark_picker_list_item.xml b/app/src/main/res/layout/bookmark_picker_list_item.xml new file mode 100644 index 0000000..95398b4 --- /dev/null +++ b/app/src/main/res/layout/bookmark_picker_list_item.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2008 ZXing authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:padding="@dimen/half_padding"> + + <TextView android:id="@+id/bookmark_title" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceLarge" + android:singleLine="true" + android:textIsSelectable="false"/> + + <TextView android:id="@+id/bookmark_url" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceSmall" + android:singleLine="false" + android:textIsSelectable="false"/> + +</LinearLayout> diff --git a/app/src/main/res/layout/capture.xml b/app/src/main/res/layout/capture.xml new file mode 100644 index 0000000..658e0f3 --- /dev/null +++ b/app/src/main/res/layout/capture.xml @@ -0,0 +1,204 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2008 ZXing authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> +<merge xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools"> + + <SurfaceView android:id="@+id/preview_view" + android:layout_width="fill_parent" + android:layout_height="fill_parent"/> + + <com.google.zxing.client.android.ViewfinderView + android:id="@+id/viewfinder_view" + android:layout_width="fill_parent" + android:layout_height="fill_parent"/> + + <LinearLayout android:id="@+id/result_view" + android:orientation="vertical" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:background="@color/result_view" + android:visibility="gone" + android:baselineAligned="false"> + + <LinearLayout + android:orientation="horizontal" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:gravity="center" + android:padding="@dimen/standard_padding"> + + <LinearLayout + android:orientation="vertical" + android:layout_width="wrap_content" + android:layout_height="fill_parent" + android:gravity="right|center_vertical"> + + <ImageView android:id="@+id/barcode_image_view" + android:layout_width="160dip" + android:layout_height="wrap_content" + android:maxWidth="160dip" + android:maxHeight="160dip" + android:layout_marginBottom="@dimen/half_padding" + android:adjustViewBounds="true" + android:scaleType="centerInside" + tools:ignore="ContentDescription"/> + + <LinearLayout + android:orientation="horizontal" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <TextView android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/msg_default_format" + android:textColor="@color/result_minor_text" + android:textStyle="bold" + android:paddingRight="@dimen/half_padding"/> + + <TextView android:id="@+id/format_text_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/result_minor_text"/> + + </LinearLayout> + + <LinearLayout + android:orientation="horizontal" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <TextView android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/msg_default_type" + android:textColor="@color/result_minor_text" + android:textStyle="bold" + android:paddingRight="@dimen/half_padding"/> + + <TextView android:id="@+id/type_text_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/result_minor_text"/> + + </LinearLayout> + + <LinearLayout + android:orientation="horizontal" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <TextView android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/msg_default_time" + android:textColor="@color/result_minor_text" + android:textStyle="bold" + android:paddingRight="@dimen/half_padding"/> + + <TextView android:id="@+id/time_text_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/result_minor_text"/> + + </LinearLayout> + + <LinearLayout + android:orientation="horizontal" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <TextView android:id="@+id/meta_text_view_label" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/msg_default_meta" + android:textColor="@color/result_minor_text" + android:textStyle="bold" + android:paddingRight="@dimen/half_padding"/> + + <TextView android:id="@+id/meta_text_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/result_minor_text"/> + + </LinearLayout> + + </LinearLayout> + + <ScrollView + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <TextView android:id="@+id/contents_text_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/result_text" + android:textColorLink="@color/result_text" + android:textSize="22sp" + android:paddingLeft="12dip" + android:autoLink="web" + android:textIsSelectable="true"/> + + <TextView android:id="@+id/contents_supplement_text_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/result_text" + android:textColorLink="@color/result_text" + android:paddingLeft="12dip" + android:autoLink="web" + android:textIsSelectable="true"/> + + </LinearLayout> + + </ScrollView> + + </LinearLayout> + + <LinearLayout android:id="@+id/result_button_view" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:gravity="center"> + + <Button style="@style/ResultButton" + android:visibility="gone"/> + + <Button style="@style/ResultButton" + android:visibility="gone"/> + + <Button style="@style/ResultButton" + android:visibility="gone"/> + + <Button style="@style/ResultButton" + android:visibility="gone"/> + + </LinearLayout> + + </LinearLayout> + + <TextView android:id="@+id/status_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="bottom|center_horizontal" + android:background="@color/transparent" + android:text="@string/msg_default_status" + android:textColor="@color/status_text"/> + +</merge> diff --git a/app/src/main/res/layout/encode.xml b/app/src/main/res/layout/encode.xml new file mode 100644 index 0000000..120935f --- /dev/null +++ b/app/src/main/res/layout/encode.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2008 ZXing authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:fillViewport="true" + android:background="@color/encode_view" + android:orientation="vertical" + android:gravity="center" + tools:ignore="Overdraw"> + + <ImageView android:id="@+id/image_view" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:scaleType="center" + tools:ignore="ContentDescription"/> + + <ScrollView android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:gravity="center"> + + <TextView android:id="@+id/contents_text_view" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:gravity="center" + android:textColor="@color/contents_text" + android:paddingBottom="@dimen/standard_padding" + android:paddingLeft="@dimen/standard_padding" + android:paddingRight="@dimen/standard_padding" + android:textIsSelectable="true"/> + + </ScrollView> + +</LinearLayout> diff --git a/app/src/main/res/layout/help.xml b/app/src/main/res/layout/help.xml new file mode 100644 index 0000000..786a624 --- /dev/null +++ b/app/src/main/res/layout/help.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2008 ZXing authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> +<WebView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/help_contents" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:layout_weight="1"/> \ No newline at end of file diff --git a/app/src/main/res/layout/history_list_item.xml b/app/src/main/res/layout/history_list_item.xml new file mode 100644 index 0000000..0c060b4 --- /dev/null +++ b/app/src/main/res/layout/history_list_item.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2008 ZXing authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:padding="@dimen/standard_padding"> + + <TextView android:id="@+id/history_title" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceLarge" + android:singleLine="true" + android:textIsSelectable="false"/> + + <TextView android:id="@+id/history_detail" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceSmall" + android:singleLine="false" + android:textIsSelectable="false"/> + +</LinearLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/search_book_contents.xml b/app/src/main/res/layout/search_book_contents.xml new file mode 100644 index 0000000..f572a0d --- /dev/null +++ b/app/src/main/res/layout/search_book_contents.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2008 ZXing authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:orientation="vertical" + android:layout_width="fill_parent" + android:layout_height="fill_parent"> + + <LinearLayout + android:orientation="horizontal" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_weight="0"> + + <EditText android:id="@+id/query_text_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="left|center_vertical" + android:layout_weight="1" + android:singleLine="true" + android:selectAllOnFocus="true" + android:inputType="text" + tools:ignore="NestedWeights"/> + + <Button android:id="@+id/query_button" + style="@android:style/Widget.Holo.Button.Borderless.Small" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="0" + android:text="@string/button_search_book_contents"/> + + </LinearLayout> + + + <ListView android:id="@+id/result_list_view" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_weight="1"/> + +</LinearLayout> diff --git a/app/src/main/res/layout/search_book_contents_header.xml b/app/src/main/res/layout/search_book_contents_header.xml new file mode 100644 index 0000000..09d627e --- /dev/null +++ b/app/src/main/res/layout/search_book_contents_header.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2008 ZXing authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> +<TextView xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:paddingLeft="@dimen/standard_padding" + android:paddingBottom="@dimen/half_padding" + android:enabled="false" + android:singleLine="true" + android:textIsSelectable="false"/> diff --git a/app/src/main/res/layout/search_book_contents_list_item.xml b/app/src/main/res/layout/search_book_contents_list_item.xml new file mode 100644 index 0000000..0238154 --- /dev/null +++ b/app/src/main/res/layout/search_book_contents_list_item.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2008 ZXing authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> +<com.google.zxing.client.android.book.SearchBookContentsListItem + xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="horizontal" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:padding="@dimen/standard_padding"> + + <TextView android:id="@+id/page_number_view" + android:layout_width="75dip" + android:layout_height="wrap_content" + android:layout_gravity="left|top" + android:layout_marginRight="@dimen/standard_padding" + android:singleLine="false" + android:textStyle="bold" + android:textIsSelectable="false"/> + + <TextView android:id="@+id/snippet_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="left|top" + android:singleLine="false" + android:textIsSelectable="false"/> + +</com.google.zxing.client.android.book.SearchBookContentsListItem> diff --git a/app/src/main/res/layout/share.xml b/app/src/main/res/layout/share.xml new file mode 100644 index 0000000..785cd0f --- /dev/null +++ b/app/src/main/res/layout/share.xml @@ -0,0 +1,95 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2008 ZXing authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<!-- ScrollView wrapper is to accommodate small screens. --> +<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:gravity="center"> + + <!-- Must wrap the rest in one layout --> + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center" + android:orientation="vertical" + android:padding="@dimen/standard_padding"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center" + android:text="@string/msg_share_explanation" + android:paddingBottom="@dimen/standard_padding" + android:textIsSelectable="false"/> + + <ImageView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:scaleType="center" + android:src="@drawable/share_via_barcode" + android:paddingBottom="@dimen/standard_padding" + tools:ignore="ContentDescription"/> + + <LinearLayout + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:paddingBottom="@dimen/standard_padding"> + + <Button android:id="@+id/share_app_button" + style="@style/ShareButton" + android:layout_weight="1" + android:text="@string/button_share_app"/> + + <Button android:id="@+id/share_bookmark_button" + style="@style/ShareButton" + android:layout_weight="1" + android:text="@string/button_share_bookmark"/> + + </LinearLayout> + + <LinearLayout + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:paddingBottom="@dimen/standard_padding"> + + <Button android:id="@+id/share_contact_button" + style="@style/ShareButton" + android:layout_weight="1" + android:text="@string/button_share_contact"/> + + <Button android:id="@+id/share_clipboard_button" + style="@style/ShareButton" + android:layout_weight="1" + android:text="@string/button_share_clipboard"/> + + </LinearLayout> + + <EditText android:id="@+id/share_text_view" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:hint="@string/msg_share_text" + android:singleLine="true" + android:selectAllOnFocus="true"/> + + </LinearLayout> + +</ScrollView> \ No newline at end of file diff --git a/app/src/main/res/menu/capture.xml b/app/src/main/res/menu/capture.xml new file mode 100644 index 0000000..fef1ad4 --- /dev/null +++ b/app/src/main/res/menu/capture.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2012 ZXing authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@+id/menu_share" + android:title="@string/menu_share" + android:icon="@android:drawable/ic_menu_share" + android:orderInCategory="1" + android:showAsAction="withText|ifRoom"/> + <item android:id="@+id/menu_history" + android:title="@string/menu_history" + android:icon="@android:drawable/ic_menu_recent_history" + android:orderInCategory="2" + android:showAsAction="withText|ifRoom"/> + <item android:id="@+id/menu_settings" + android:title="@string/menu_settings" + android:icon="@android:drawable/ic_menu_preferences" + android:orderInCategory="3" + android:showAsAction="withText"/> + <item android:id="@+id/menu_help" + android:title="@string/menu_help" + android:icon="@android:drawable/ic_menu_help" + android:orderInCategory="4" + android:showAsAction="withText"/> +</menu> \ No newline at end of file diff --git a/app/src/main/res/menu/encode.xml b/app/src/main/res/menu/encode.xml new file mode 100644 index 0000000..b6b1237 --- /dev/null +++ b/app/src/main/res/menu/encode.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2012 ZXing authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@+id/menu_share" + android:title="@string/menu_share" + android:icon="@android:drawable/ic_menu_share" + android:orderInCategory="1" + android:showAsAction="withText|ifRoom"/> + <item android:id="@+id/menu_encode" + android:title="@string/menu_encode_vcard" + android:icon="@android:drawable/ic_menu_sort_alphabetically" + android:orderInCategory="2" + android:showAsAction="withText|ifRoom"/> +</menu> \ No newline at end of file diff --git a/app/src/main/res/menu/history.xml b/app/src/main/res/menu/history.xml new file mode 100644 index 0000000..17faa05 --- /dev/null +++ b/app/src/main/res/menu/history.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2012 ZXing authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@+id/menu_history_send" + android:title="@string/history_send" + android:icon="@android:drawable/ic_menu_share" + android:orderInCategory="1" + android:showAsAction="withText|ifRoom"/> + <item android:id="@+id/menu_history_clear_text" + android:title="@string/history_clear_text" + android:icon="@android:drawable/ic_menu_delete" + android:orderInCategory="2" + android:showAsAction="withText|ifRoom"/> +</menu> \ No newline at end of file diff --git a/app/src/main/res/raw/beep.ogg b/app/src/main/res/raw/beep.ogg new file mode 100644 index 0000000..1419947 Binary files /dev/null and b/app/src/main/res/raw/beep.ogg differ diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml new file mode 100644 index 0000000..08959d5 --- /dev/null +++ b/app/src/main/res/values/arrays.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2008 ZXing authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> +<resources xmlns:tools="http://schemas.android.com/tools"> + <string-array name="country_codes" tools:ignore="MissingTranslation"> + <item>-</item> + <item>AR</item> + <item>AU</item> + <item>BR</item> + <item>BG</item> + <item>CA</item> + <item>CH</item> + <item>CN</item> + <item>CZ</item> + <item>DE</item> + <item>DK</item> + <item>ES</item> + <item>FI</item> + <item>FR</item> + <item>GB</item> + <item>GR</item> + <item>HU</item> + <item>ID</item> + <item>IT</item> + <item>JP</item> + <item>KR</item> + <item>NL</item> + <item>PL</item> + <item>PT</item> + <item>RO</item> + <item>RU</item> + <item>SE</item> + <item>SK</item> + <item>SI</item> + <item>TR</item> + <item>TW</item> + <item>US</item> + </string-array> + <string-array name="preferences_front_light_values" tools:ignore="MissingTranslation"> + <item>ON</item> + <item>AUTO</item> + <item>OFF</item> + </string-array> + <string-array name="preferences_front_light_options"> + <item>@string/preferences_front_light_on</item> + <item>@string/preferences_front_light_auto</item> + <item>@string/preferences_front_light_off</item> + </string-array> +</resources> diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 3ab3e9c..90633b0 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -3,4 +3,15 @@ <color name="colorPrimary">#3F51B5</color> <color name="colorPrimaryDark">#303F9F</color> <color name="colorAccent">#FF4081</color> + <color name="contents_text">#ff000000</color> + <color name="encode_view">#ffffffff</color> + <color name="possible_result_points">#c0ffbd21</color> <!-- Android standard ICS color --> + <color name="result_minor_text">#ffc0c0c0</color> + <color name="result_points">#c099cc00</color> <!-- Android standard ICS color --> + <color name="result_text">#ffffffff</color> + <color name="result_view">#b0000000</color> + <color name="status_text">#ffffffff</color> + <color name="transparent">#00000000</color> + <color name="viewfinder_laser">#ffcc0000</color> <!-- Android standard ICS color --> + <color name="viewfinder_mask">#60000000</color> </resources> diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index e742b28..869e78e 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -4,4 +4,9 @@ <dimen name="app_bar_height">75dp</dimen> <dimen name="item_width">200dp</dimen> <dimen name="text_margin">16dp</dimen> + <!-- Default screen margins, per the Android Design guidelines. --> + <dimen name="activity_horizontal_margin">16dp</dimen> + <dimen name="activity_vertical_margin">16dp</dimen> + <dimen name="standard_padding">8dip</dimen> + <dimen name="half_padding">4dip</dimen> </resources> diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml new file mode 100644 index 0000000..58399b9 --- /dev/null +++ b/app/src/main/res/values/ids.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2008 ZXing authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> +<resources> + <item type="id" name="decode"/> + <item type="id" name="decode_failed"/> + <item type="id" name="decode_succeeded"/> + <item type="id" name="launch_product_query"/> + <item type="id" name="quit"/> + <item type="id" name="restart_preview"/> + <item type="id" name="return_scan_result"/> +</resources> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9684ed6..0b0ce68 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,128 @@ <resources> <string name="app_name">PillDroid</string> <string name="title_medicament_detail">Medicament Detail</string> + <string name="app_picker_name">Applications</string> + <string name="bookmark_picker_name">Bookmarks</string> + <string name="button_add_calendar">Add to calendar</string> + <string name="button_add_contact">Add contact</string> + <string name="button_book_search">Book Search</string> + <string name="button_cancel">Cancel</string> + <string name="button_custom_product_search">Custom search</string> + <string name="button_dial">Dial number</string> + <string name="button_email">Send email</string> + <string name="button_get_directions">Get directions</string> + <string name="button_mms">Send MMS</string> + <string name="button_ok">OK</string> + <string name="button_open_browser">Open browser</string> + <string name="button_product_search">Product search</string> + <string name="button_search_book_contents">Search contents</string> + <string name="button_share_app">Application</string> + <string name="button_share_bookmark">Bookmark</string> + <string name="button_share_by_email">Share via email</string> + <string name="button_share_by_sms">Share via SMS</string> + <string name="button_share_clipboard">Clipboard</string> + <string name="button_share_contact">Contact</string> + <string name="button_show_map">Show map</string> + <string name="button_sms">Send SMS</string> + <string name="button_web_search">Web search</string> + <string name="button_wifi">Connect to Network</string> + <string name="contents_contact">Contact info</string> + <string name="contents_email">Email address</string> + <string name="contents_location">Geographic coordinates</string> + <string name="contents_phone">Phone number</string> + <string name="contents_sms">SMS address</string> + <string name="contents_text">Plain text</string> + <string name="history_clear_text">Clear history</string> + <string name="history_clear_one_history_text">Clear</string> + <string name="history_email_title">Barcode Scanner history</string> + <string name="history_empty">Empty</string> + <string name="history_empty_detail">No barcode scans have been recorded</string> + <string name="history_send">Send history</string> + <string name="history_title">History</string> + <string name="menu_encode_mecard">Use MECARD</string> + <string name="menu_encode_vcard">Use vCard</string> + <string name="menu_help">Help</string> + <string name="menu_history">History</string> + <string name="menu_settings">Settings</string> + <string name="menu_share">Share</string> + <string name="msg_bulk_mode_scanned">Bulk mode: barcode scanned and saved</string> + <string name="msg_camera_framework_bug">Sorry, the Android camera encountered a problem. You may need to restart the device.</string> + <string name="msg_default_format">Format</string> + <string name="msg_default_meta">Metadata</string> + <string name="msg_default_mms_subject">Hi</string> + <string name="msg_default_status">Place a barcode inside the viewfinder rectangle to scan it.</string> + <string name="msg_default_time">Time</string> + <string name="msg_default_type">Type</string> + <string name="msg_encode_contents_failed">Could not encode a barcode from the data provided.</string> + <string name="msg_error">Error</string> + <string name="msg_google_books">Google</string> + <string name="msg_google_product">Google</string> + <string name="msg_intent_failed">Sorry, the requested application could not be launched. The barcode contents may be invalid.</string> + <string name="msg_invalid_value">Invalid value</string> + <string name="msg_redirect">Redirect</string> + <string name="msg_sbc_book_not_searchable">Sorry, this book is not searchable.</string> + <string name="msg_sbc_failed">Sorry, the search encountered a problem.</string> + <string name="msg_sbc_no_page_returned">No page returned</string> + <string name="msg_sbc_page">Page</string> + <string name="msg_sbc_results">Results</string> + <string name="msg_sbc_searching_book">Searching book\u2026</string> + <string name="msg_sbc_snippet_unavailable">Snippet not available</string> + <string name="msg_share_explanation">You can share data by displaying a barcode on your screen and scanning it with another phone.</string> + <string name="msg_share_text">Or type some text and press Enter</string> + <string name="msg_sure">Are you sure?</string> + <string name="msg_unmount_usb">Sorry, the SD card is not accessible.</string> + <string name="preferences_actions_title">When a barcode is found\u2026</string> + <string name="preferences_auto_focus_title">Use auto focus</string> + <string name="preferences_auto_open_web_title">Open web pages automatically</string> + <string name="preferences_bulk_mode_summary">Scan and save many barcodes continuously</string> + <string name="preferences_bulk_mode_title">Bulk scan mode</string> + <string name="preferences_copy_to_clipboard_title">Copy to clipboard</string> + <string name="preferences_custom_product_search_summary" formatted="false">Substitutions: %s = contents, %f = format, %t = type</string> + <string name="preferences_custom_product_search_title">Custom search URL</string> + <string name="preferences_decode_1D_industrial_title">1D Industrial</string> + <string name="preferences_decode_1D_product_title">1D Product</string> + <string name="preferences_decode_Aztec_title">Aztec</string> + <string name="preferences_decode_Data_Matrix_title">Data Matrix</string> + <string name="preferences_decode_PDF417_title">PDF417 (β)</string> + <string name="preferences_decode_QR_title">QR Codes</string> + <string name="preferences_device_bug_workarounds_title">Device Bug Workarounds</string> + <string name="preferences_disable_barcode_scene_mode_title">No barcode scene mode</string> + <string name="preferences_disable_continuous_focus_summary">Use only standard focus mode</string> + <string name="preferences_disable_continuous_focus_title">No continuous focus</string> + <string name="preferences_disable_exposure_title">No exposure</string> + <string name="preferences_disable_metering_title">No metering</string> + <string name="preferences_front_light_summary">Improves scanning in low light on some phones, but may cause glare. Does not work on all phones.</string> + <string name="preferences_front_light_title">Use front light</string> + <string name="preferences_front_light_auto">Automatic</string> + <string name="preferences_front_light_off">Off</string> + <string name="preferences_front_light_on">On</string> + <string name="preferences_general_title">General settings</string> + <string name="preferences_history_summary">Store your scans in History</string> + <string name="preferences_history_title">Add to History</string> + <string name="preferences_invert_scan_title">Invert scan</string> + <string name="preferences_invert_scan_summary">Scan for white barcodes on black background. Not available on some devices.</string> + <string name="preferences_name">Settings</string> + <string name="preferences_orientation_title">No automatic rotation</string> + <string name="preferences_play_beep_title">Beep</string> + <string name="preferences_remember_duplicates_summary">Store multiple scans of the same barcode in History</string> + <string name="preferences_remember_duplicates_title">Remember duplicates</string> + <string name="preferences_result_title">Result settings</string> + <string name="preferences_scanning_title">When scanning for barcodes, decode\u2026</string> + <string name="preferences_search_country">Search country</string> + <string name="preferences_supplemental_summary">Try to retrieve more information about the barcode contents</string> + <string name="preferences_supplemental_title">Retrieve more info</string> + <string name="preferences_vibrate_title">Vibrate</string> + <string name="result_address_book">Found contact info</string> + <string name="result_calendar">Found calendar event</string> + <string name="result_email_address">Found email address</string> + <string name="result_geo">Found geographic coordinates</string> + <string name="result_isbn">Found book</string> + <string name="result_product">Found product</string> + <string name="result_sms">Found SMS address</string> + <string name="result_tel">Found phone number</string> + <string name="result_text">Found plain text</string> + <string name="result_uri">Found URL</string> + <string name="result_wifi">Found WLAN Configuration</string> + <string name="sbc_name">Google Book Search</string> + <string name="wifi_changing_network">Requesting connection to network\u2026</string> </resources> diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 545b9c6..8a9ee49 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -16,5 +16,13 @@ <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" /> <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" /> - +<style name="ShareButton" parent="@android:style/Widget.Holo.Button.Borderless.Small"> + <item name="android:layout_width">wrap_content</item> + <item name="android:layout_height">wrap_content</item> + </style> +<style name="ResultButton" parent="@android:style/Widget.Holo.Button.Borderless.Small"> + <item name="android:layout_width">0dip</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:layout_weight">1</item> +</style> </resources> diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml new file mode 100644 index 0000000..196fed1 --- /dev/null +++ b/app/src/main/res/xml/preferences.xml @@ -0,0 +1,135 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2008 ZXing authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> +<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> + <PreferenceCategory android:title="@string/preferences_scanning_title"> + <CheckBoxPreference + android:key="preferences_decode_1D_product" + android:defaultValue="true" + android:title="@string/preferences_decode_1D_product_title"/> + <CheckBoxPreference + android:key="preferences_decode_1D_industrial" + android:defaultValue="true" + android:title="@string/preferences_decode_1D_industrial_title"/> + <CheckBoxPreference + android:key="preferences_decode_QR" + android:defaultValue="true" + android:title="@string/preferences_decode_QR_title"/> + <CheckBoxPreference + android:key="preferences_decode_Data_Matrix" + android:defaultValue="true" + android:title="@string/preferences_decode_Data_Matrix_title"/> + <CheckBoxPreference + android:key="preferences_decode_Aztec" + android:defaultValue="false" + android:title="@string/preferences_decode_Aztec_title"/> + <CheckBoxPreference + android:key="preferences_decode_PDF417" + android:defaultValue="false" + android:title="@string/preferences_decode_PDF417_title"/> + </PreferenceCategory> + <PreferenceCategory android:title="@string/preferences_actions_title"> + <CheckBoxPreference + android:key="preferences_play_beep" + android:defaultValue="true" + android:title="@string/preferences_play_beep_title"/> + <CheckBoxPreference + android:key="preferences_vibrate" + android:defaultValue="false" + android:title="@string/preferences_vibrate_title"/> + <CheckBoxPreference + android:key="preferences_copy_to_clipboard" + android:defaultValue="true" + android:title="@string/preferences_copy_to_clipboard_title"/> + <CheckBoxPreference + android:key="preferences_auto_open_web" + android:defaultValue="false" + android:title="@string/preferences_auto_open_web_title"/> + <CheckBoxPreference + android:key="preferences_remember_duplicates" + android:defaultValue="false" + android:title="@string/preferences_remember_duplicates_title" + android:summary="@string/preferences_remember_duplicates_summary"/> + <CheckBoxPreference + android:key="preferences_history" + android:defaultValue="true" + android:title="@string/preferences_history_title" + android:summary="@string/preferences_history_summary"/> + <CheckBoxPreference + android:key="preferences_supplemental" + android:defaultValue="true" + android:title="@string/preferences_supplemental_title" + android:summary="@string/preferences_supplemental_summary"/> + </PreferenceCategory> + <PreferenceCategory android:title="@string/preferences_general_title"> + <ListPreference + android:entries="@array/preferences_front_light_options" + android:entryValues="@array/preferences_front_light_values" + android:key="preferences_front_light_mode" + android:defaultValue="OFF" + android:title="@string/preferences_front_light_title" + android:summary="@string/preferences_front_light_summary"/> + <CheckBoxPreference + android:key="preferences_auto_focus" + android:defaultValue="true" + android:title="@string/preferences_auto_focus_title"/> + <CheckBoxPreference + android:key="preferences_invert_scan" + android:defaultValue="false" + android:title="@string/preferences_invert_scan_title" + android:summary="@string/preferences_invert_scan_summary"/> + <CheckBoxPreference + android:key="preferences_bulk_mode" + android:defaultValue="false" + android:title="@string/preferences_bulk_mode_title" + android:summary="@string/preferences_bulk_mode_summary"/> + <CheckBoxPreference + android:key="preferences_orientation" + android:defaultValue="true" + android:title="@string/preferences_orientation_title"/> + </PreferenceCategory> + <PreferenceCategory android:title="@string/preferences_result_title"> + <EditTextPreference + android:key="preferences_custom_product_search" + android:title="@string/preferences_custom_product_search_title" + android:summary="@string/preferences_custom_product_search_summary"/> + <ListPreference + android:key="preferences_search_country" + android:defaultValue="-" + android:entries="@array/country_codes" + android:entryValues="@array/country_codes" + android:title="@string/preferences_search_country"/> + </PreferenceCategory> + <PreferenceCategory android:title="@string/preferences_device_bug_workarounds_title"> + <CheckBoxPreference + android:key="preferences_disable_continuous_focus" + android:defaultValue="true" + android:title="@string/preferences_disable_continuous_focus_title" + android:summary="@string/preferences_disable_continuous_focus_summary"/> + <CheckBoxPreference + android:key="preferences_disable_exposure" + android:defaultValue="true" + android:title="@string/preferences_disable_exposure_title"/> + <CheckBoxPreference + android:key="preferences_disable_metering" + android:defaultValue="true" + android:title="@string/preferences_disable_metering_title"/> + <CheckBoxPreference + android:key="preferences_disable_barcode_scene_mode" + android:defaultValue="true" + android:title="@string/preferences_disable_barcode_scene_mode_title"/> + </PreferenceCategory> +</PreferenceScreen>