| /* |
| * Copyright (C) 2018 The Android Open Source Project |
| * |
| * 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.android.startop.iorap; |
| // TODO: rename to com.android.server.startop.iorap |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.app.job.JobInfo; |
| import android.app.job.JobParameters; |
| import android.app.job.JobService; |
| import android.app.job.JobScheduler; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.ActivityInfo; |
| import android.os.IBinder; |
| import android.os.IBinder.DeathRecipient; |
| import android.os.Handler; |
| import android.os.Parcel; |
| import android.os.RemoteException; |
| import android.os.ServiceManager; |
| import android.os.SystemProperties; |
| import android.util.Log; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.server.IoThread; |
| import com.android.server.LocalServices; |
| import com.android.server.SystemService; |
| import com.android.server.wm.ActivityMetricsLaunchObserver; |
| import com.android.server.wm.ActivityMetricsLaunchObserver.ActivityRecordProto; |
| import com.android.server.wm.ActivityMetricsLaunchObserver.Temperature; |
| import com.android.server.wm.ActivityMetricsLaunchObserverRegistry; |
| import com.android.server.wm.ActivityTaskManagerInternal; |
| |
| import java.util.concurrent.TimeUnit; |
| import java.util.HashMap; |
| |
| /** |
| * System-server-local proxy into the {@code IIorap} native service. |
| */ |
| public class IorapForwardingService extends SystemService { |
| |
| public static final String TAG = "IorapForwardingService"; |
| /** $> adb shell 'setprop log.tag.IorapForwardingService VERBOSE' */ |
| public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); |
| /** $> adb shell 'setprop ro.iorapd.enable true' */ |
| private static boolean IS_ENABLED = SystemProperties.getBoolean("ro.iorapd.enable", true); |
| /** $> adb shell 'setprop iorapd.forwarding_service.wtf_crash true' */ |
| private static boolean WTF_CRASH = SystemProperties.getBoolean( |
| "iorapd.forwarding_service.wtf_crash", false); |
| |
| // "Unique" job ID from the service name. Also equal to 283673059. |
| public static final int JOB_ID_IORAPD = encodeEnglishAlphabetStringIntoInt("iorapd"); |
| // Run every 24 hours. |
| public static final long JOB_INTERVAL_MS = TimeUnit.HOURS.toMillis(24); |
| |
| private IIorap mIorapRemote; |
| private final Object mLock = new Object(); |
| /** Handle onBinderDeath by periodically trying to reconnect. */ |
| private final Handler mHandler = |
| new BinderConnectionHandler(IoThread.getHandler().getLooper()); |
| |
| private volatile IorapdJobService mJobService; // Write-once (null -> non-null forever). |
| private volatile static IorapForwardingService sSelfService; // Write once (null -> non-null). |
| |
| /** |
| * Initializes the system service. |
| * <p> |
| * Subclasses must define a single argument constructor that accepts the context |
| * and passes it to super. |
| * </p> |
| * |
| * @param context The system server context. |
| */ |
| public IorapForwardingService(Context context) { |
| super(context); |
| |
| if (DEBUG) { |
| Log.v(TAG, "IorapForwardingService (Context=" + context.toString() + ")"); |
| } |
| |
| if (sSelfService != null) { |
| throw new AssertionError("only one service instance allowed"); |
| } |
| sSelfService = this; |
| } |
| |
| //<editor-fold desc="Providers"> |
| /* |
| * Providers for external dependencies: |
| * - These are marked as protected to allow tests to inject different values via mocks. |
| */ |
| |
| @VisibleForTesting |
| protected ActivityMetricsLaunchObserverRegistry provideLaunchObserverRegistry() { |
| ActivityTaskManagerInternal amtInternal = |
| LocalServices.getService(ActivityTaskManagerInternal.class); |
| ActivityMetricsLaunchObserverRegistry launchObserverRegistry = |
| amtInternal.getLaunchObserverRegistry(); |
| return launchObserverRegistry; |
| } |
| |
| @VisibleForTesting |
| protected IIorap provideIorapRemote() { |
| IIorap iorap; |
| try { |
| iorap = IIorap.Stub.asInterface(ServiceManager.getServiceOrThrow("iorapd")); |
| } catch (ServiceManager.ServiceNotFoundException e) { |
| handleRemoteError(e); |
| return null; |
| } |
| |
| try { |
| iorap.asBinder().linkToDeath(provideDeathRecipient(), /*flags*/0); |
| } catch (RemoteException e) { |
| handleRemoteError(e); |
| return null; |
| } |
| |
| return iorap; |
| } |
| |
| @VisibleForTesting |
| protected DeathRecipient provideDeathRecipient() { |
| return new DeathRecipient() { |
| @Override |
| public void binderDied() { |
| Log.w(TAG, "iorapd has died"); |
| retryConnectToRemoteAndConfigure(/*attempts*/0); |
| |
| if (mJobService != null) { |
| mJobService.onIorapdDisconnected(); |
| } |
| } |
| }; |
| } |
| |
| @VisibleForTesting |
| protected boolean isIorapEnabled() { |
| // Same as the property in iorapd.rc -- disabling this will mean the 'iorapd' binder process |
| // never comes up, so all binder connections will fail indefinitely. |
| return IS_ENABLED; |
| } |
| |
| //</editor-fold> |
| |
| @Override |
| public void onStart() { |
| if (DEBUG) { |
| Log.v(TAG, "onStart"); |
| } |
| |
| retryConnectToRemoteAndConfigure(/*attempts*/0); |
| } |
| |
| @Override |
| public void onBootPhase(int phase) { |
| if (phase == PHASE_BOOT_COMPLETED) { |
| if (DEBUG) { |
| Log.v(TAG, "onBootPhase(PHASE_BOOT_COMPLETED)"); |
| } |
| |
| if (isIorapEnabled()) { |
| // Set up a recurring background job. This has to be done in a later phase since it |
| // has a dependency the job scheduler. |
| // |
| // Doing this too early can result in a ServiceNotFoundException for 'jobservice' |
| // or a null reference for #getSystemService(JobScheduler.class) |
| mJobService = new IorapdJobService(getContext()); |
| } |
| } |
| } |
| |
| private class BinderConnectionHandler extends Handler { |
| public BinderConnectionHandler(android.os.Looper looper) { |
| super(looper); |
| } |
| |
| public static final int MESSAGE_BINDER_CONNECT = 0; |
| |
| private int mAttempts = 0; |
| |
| @Override |
| public void handleMessage(android.os.Message message) { |
| switch (message.what) { |
| case MESSAGE_BINDER_CONNECT: |
| if (!retryConnectToRemoteAndConfigure(mAttempts)) { |
| mAttempts++; |
| } else { |
| mAttempts = 0; |
| } |
| break; |
| default: |
| throw new AssertionError("Unknown message: " + message.toString()); |
| } |
| } |
| } |
| |
| /** |
| * Handle iorapd shutdowns and crashes, by attempting to reconnect |
| * until the service is reached again. |
| * |
| * <p>The first connection attempt is synchronous, |
| * subsequent attempts are done by posting delayed tasks to the IoThread.</p> |
| * |
| * @return true if connection succeeded now, or false if it failed now [and needs to requeue]. |
| */ |
| private boolean retryConnectToRemoteAndConfigure(int attempts) { |
| final int sleepTime = 1000; // ms |
| |
| if (DEBUG) { |
| Log.v(TAG, "retryConnectToRemoteAndConfigure - attempt #" + attempts); |
| } |
| |
| if (connectToRemoteAndConfigure()) { |
| return true; |
| } |
| |
| // Either 'iorapd' is stuck in a crash loop (ouch!!) or we manually |
| // called 'adb shell stop iorapd' , which means this would loop until it comes back |
| // up. |
| // |
| // TODO: it would be good to get nodified of 'adb shell stop iorapd' to avoid |
| // printing this warning. |
| Log.w(TAG, "Failed to connect to iorapd, is it down? Delay for " + sleepTime); |
| |
| // Use a handler instead of Thread#sleep to avoid backing up the binder thread |
| // when this is called from the death recipient callback. |
| mHandler.sendMessageDelayed( |
| mHandler.obtainMessage(BinderConnectionHandler.MESSAGE_BINDER_CONNECT), |
| sleepTime); |
| |
| return false; |
| |
| // Log.e(TAG, "Can't connect to iorapd - giving up after " + attempts + " attempts"); |
| } |
| |
| private boolean connectToRemoteAndConfigure() { |
| synchronized (mLock) { |
| // Synchronize against any concurrent calls to this via the DeathRecipient. |
| return connectToRemoteAndConfigureLocked(); |
| } |
| } |
| |
| private boolean connectToRemoteAndConfigureLocked() { |
| if (!isIorapEnabled()) { |
| if (DEBUG) { |
| Log.v(TAG, "connectToRemoteAndConfigure - iorapd is disabled, skip rest of work"); |
| } |
| // When we see that iorapd is disabled (when system server comes up), |
| // it stays disabled permanently until the next system server reset. |
| |
| // TODO: consider listening to property changes as a callback, then we can |
| // be more dynamic about handling enable/disable. |
| return true; |
| } |
| |
| // Connect to the native binder service. |
| mIorapRemote = provideIorapRemote(); |
| if (mIorapRemote == null) { |
| Log.e(TAG, "connectToRemoteAndConfigure - null iorap remote. check for Log.wtf?"); |
| return false; |
| } |
| invokeRemote(mIorapRemote, |
| (IIorap remote) -> remote.setTaskListener(new RemoteTaskListener()) ); |
| registerInProcessListenersLocked(); |
| |
| return true; |
| } |
| |
| private final AppLaunchObserver mAppLaunchObserver = new AppLaunchObserver(); |
| private boolean mRegisteredListeners = false; |
| |
| private void registerInProcessListenersLocked() { |
| if (mRegisteredListeners) { |
| // Listeners are registered only once (idempotent operation). |
| // |
| // Today listeners are tolerant of the remote side going away |
| // by handling remote errors. |
| // |
| // We could try to 'unregister' the listener when we get a binder disconnect, |
| // but we'd still have to handle the case of encountering synchronous errors so |
| // it really wouldn't be a win (other than having less log spew). |
| return; |
| } |
| |
| // Listen to App Launch Sequence events from ActivityTaskManager, |
| // and forward them to the native binder service. |
| ActivityMetricsLaunchObserverRegistry launchObserverRegistry = |
| provideLaunchObserverRegistry(); |
| launchObserverRegistry.registerLaunchObserver(mAppLaunchObserver); |
| |
| mRegisteredListeners = true; |
| } |
| |
| private class AppLaunchObserver implements ActivityMetricsLaunchObserver { |
| // We add a synthetic sequence ID here to make it easier to differentiate new |
| // launch sequences on the native side. |
| private @AppLaunchEvent.SequenceId long mSequenceId = -1; |
| |
| // All callbacks occur on the same background thread. Don't synchronize explicitly. |
| |
| @Override |
| public void onIntentStarted(@NonNull Intent intent) { |
| // #onIntentStarted [is the only transition that] initiates a new launch sequence. |
| ++mSequenceId; |
| |
| if (DEBUG) { |
| Log.v(TAG, String.format("AppLaunchObserver#onIntentStarted(%d, %s)", |
| mSequenceId, intent)); |
| } |
| |
| invokeRemote(mIorapRemote, |
| (IIorap remote) -> |
| remote.onAppLaunchEvent(RequestId.nextValueForSequence(), |
| new AppLaunchEvent.IntentStarted(mSequenceId, intent)) |
| ); |
| } |
| |
| @Override |
| public void onIntentFailed() { |
| if (DEBUG) { |
| Log.v(TAG, String.format("AppLaunchObserver#onIntentFailed(%d)", mSequenceId)); |
| } |
| |
| invokeRemote(mIorapRemote, |
| (IIorap remote) -> |
| remote.onAppLaunchEvent(RequestId.nextValueForSequence(), |
| new AppLaunchEvent.IntentFailed(mSequenceId)) |
| ); |
| } |
| |
| @Override |
| public void onActivityLaunched(@NonNull @ActivityRecordProto byte[] activity, |
| @Temperature int temperature) { |
| if (DEBUG) { |
| Log.v(TAG, String.format("AppLaunchObserver#onActivityLaunched(%d, %s, %d)", |
| mSequenceId, activity, temperature)); |
| } |
| |
| invokeRemote(mIorapRemote, |
| (IIorap remote) -> |
| remote.onAppLaunchEvent(RequestId.nextValueForSequence(), |
| new AppLaunchEvent.ActivityLaunched(mSequenceId, activity, temperature)) |
| ); |
| } |
| |
| @Override |
| public void onActivityLaunchCancelled(@Nullable @ActivityRecordProto byte[] activity) { |
| if (DEBUG) { |
| Log.v(TAG, String.format("AppLaunchObserver#onActivityLaunchCancelled(%d, %s)", |
| mSequenceId, activity)); |
| } |
| |
| invokeRemote(mIorapRemote, |
| (IIorap remote) -> |
| remote.onAppLaunchEvent(RequestId.nextValueForSequence(), |
| new AppLaunchEvent.ActivityLaunchCancelled(mSequenceId, |
| activity))); |
| } |
| |
| @Override |
| public void onActivityLaunchFinished(@NonNull @ActivityRecordProto byte[] activity) { |
| if (DEBUG) { |
| Log.v(TAG, String.format("AppLaunchObserver#onActivityLaunchFinished(%d, %s)", |
| mSequenceId, activity)); |
| } |
| |
| invokeRemote(mIorapRemote, |
| (IIorap remote) -> |
| remote.onAppLaunchEvent(RequestId.nextValueForSequence(), |
| new AppLaunchEvent.ActivityLaunchFinished(mSequenceId, activity)) |
| ); |
| } |
| } |
| |
| /** |
| * Debugging: |
| * |
| * $> adb shell dumpsys jobscheduler |
| * |
| * Search for 'IorapdJobServiceProxy'. |
| * |
| * JOB #1000/283673059: 6e54ed android/com.google.android.startop.iorap.IorapForwardingService$IorapdJobServiceProxy |
| * ^ ^ ^ |
| * (uid, job id) ComponentName(package/class) |
| * |
| * Forcing the job to be run, ignoring constraints: |
| * |
| * $> adb shell cmd jobscheduler run -f android 283673059 |
| * ^ ^ |
| * package job_id |
| * |
| * ------------------------------------------------------------ |
| * |
| * This class is instantiated newly by the JobService every time |
| * it wants to run a new job. |
| * |
| * We need to forward invocations to the current running instance of |
| * IorapForwardingService#IorapdJobService. |
| * |
| * Visibility: Must be accessible from android.app.AppComponentFactory |
| */ |
| public static class IorapdJobServiceProxy extends JobService { |
| |
| public IorapdJobServiceProxy() { |
| getActualIorapdJobService().bindProxy(this); |
| } |
| |
| |
| @NonNull |
| private IorapdJobService getActualIorapdJobService() { |
| // Can't ever be null, because the guarantee is that the |
| // IorapForwardingService is always running. |
| // We are in the same process as Job Service. |
| return sSelfService.mJobService; |
| } |
| |
| // Called by system to start the job. |
| @Override |
| public boolean onStartJob(JobParameters params) { |
| return getActualIorapdJobService().onStartJob(params); |
| } |
| |
| // Called by system to prematurely stop the job. |
| @Override |
| public boolean onStopJob(JobParameters params) { |
| return getActualIorapdJobService().onStopJob(params); |
| } |
| } |
| |
| private class IorapdJobService extends JobService { |
| private final ComponentName IORAPD_COMPONENT_NAME; |
| |
| private final Object mLock = new Object(); |
| // Jobs currently running remotely on iorapd. |
| // They were started by the JobScheduler and need to be finished. |
| private final HashMap<RequestId, JobParameters> mRunningJobs = new HashMap<>(); |
| |
| private final JobInfo IORAPD_JOB_INFO; |
| |
| private volatile IorapdJobServiceProxy mProxy; |
| |
| public void bindProxy(IorapdJobServiceProxy proxy) { |
| mProxy = proxy; |
| } |
| |
| // Create a new job service which immediately schedules a 24-hour idle maintenance mode |
| // background job to execute. |
| public IorapdJobService(Context context) { |
| if (DEBUG) { |
| Log.v(TAG, "IorapdJobService (Context=" + context.toString() + ")"); |
| } |
| |
| // Schedule the proxy class to be instantiated by the JobScheduler |
| // when it is time to invoke background jobs for IorapForwardingService. |
| |
| |
| // This also needs a BIND_JOB_SERVICE permission in |
| // frameworks/base/core/res/AndroidManifest.xml |
| IORAPD_COMPONENT_NAME = new ComponentName(context, IorapdJobServiceProxy.class); |
| |
| JobInfo.Builder builder = new JobInfo.Builder(JOB_ID_IORAPD, IORAPD_COMPONENT_NAME); |
| builder.setPeriodic(JOB_INTERVAL_MS); |
| builder.setPrefetch(true); |
| |
| builder.setRequiresCharging(true); |
| builder.setRequiresDeviceIdle(true); |
| |
| builder.setRequiresStorageNotLow(true); |
| |
| IORAPD_JOB_INFO = builder.build(); |
| |
| JobScheduler js = context.getSystemService(JobScheduler.class); |
| js.schedule(IORAPD_JOB_INFO); |
| Log.d(TAG, |
| "BgJob Scheduled (jobId=" + JOB_ID_IORAPD |
| + ", interval: " + JOB_INTERVAL_MS + "ms)"); |
| } |
| |
| // Called by system to start the job. |
| @Override |
| public boolean onStartJob(JobParameters params) { |
| // Tell iorapd to start a background job. |
| Log.d(TAG, "Starting background job: " + params.toString()); |
| |
| // We wait until that job's sequence ID returns to us with 'Completed', |
| RequestId request; |
| synchronized (mLock) { |
| // TODO: would be cleaner if we got the request from the 'invokeRemote' function. |
| // Better yet, consider a Pair<RequestId, Future<TaskResult>> or similar. |
| request = RequestId.nextValueForSequence(); |
| mRunningJobs.put(request, params); |
| } |
| |
| if (!invokeRemote(mIorapRemote, (IIorap remote) -> |
| remote.onJobScheduledEvent(request, |
| JobScheduledEvent.createIdleMaintenance( |
| JobScheduledEvent.TYPE_START_JOB, |
| params)) |
| )) { |
| synchronized (mLock) { |
| mRunningJobs.remove(request); // Avoid memory leaks. |
| } |
| |
| // Something went wrong on the remote side. Treat the job as being |
| // 'already finished' (i.e. immediately release wake lock). |
| return false; |
| } |
| |
| // True -> keep the wakelock acquired until #jobFinished is called. |
| return true; |
| } |
| |
| // Called by system to prematurely stop the job. |
| @Override |
| public boolean onStopJob(JobParameters params) { |
| // As this is unexpected behavior, print a warning. |
| Log.w(TAG, "onStopJob(params=" + params.toString() + ")"); |
| |
| // No longer track this job (avoids a memory leak). |
| boolean wasTracking = false; |
| synchronized (mLock) { |
| for (HashMap.Entry<RequestId, JobParameters> entry : mRunningJobs.entrySet()) { |
| if (entry.getValue().getJobId() == params.getJobId()) { |
| mRunningJobs.remove(entry.getKey()); |
| wasTracking = true; |
| } |
| } |
| } |
| |
| // Notify iorapd to stop (abort) the job. |
| if (wasTracking) { |
| invokeRemote(mIorapRemote, (IIorap remote) -> |
| remote.onJobScheduledEvent(RequestId.nextValueForSequence(), |
| JobScheduledEvent.createIdleMaintenance( |
| JobScheduledEvent.TYPE_STOP_JOB, |
| params)) |
| ); |
| } else { |
| // Even weirder. This could only be considered "correct" if iorapd reported success |
| // concurrently to the JobService requesting an onStopJob. |
| Log.e(TAG, "Untracked onStopJob request"); // see above Log.w for the params. |
| } |
| |
| |
| // Yes, retry the job at a later time no matter what. |
| return true; |
| } |
| |
| // Listen to *all* task completes for all requests. |
| // The majority of these might be unrelated to background jobs. |
| public void onIorapdTaskCompleted(RequestId requestId) { |
| JobParameters jobParameters; |
| synchronized (mLock) { |
| jobParameters = mRunningJobs.remove(requestId); |
| } |
| |
| // Typical case: This was a task callback unrelated to our jobs. |
| if (jobParameters == null) { |
| return; |
| } |
| |
| if (DEBUG) { |
| Log.v(TAG, |
| String.format("IorapdJobService#onIorapdTaskCompleted(%s), found params=%s", |
| requestId, jobParameters)); |
| } |
| |
| Log.d(TAG, "Finished background job: " + jobParameters.toString()); |
| |
| // Job is successful and periodic. Do not 'reschedule' according to the back-off |
| // criteria. |
| // |
| // This releases the wakelock that was acquired in #onStartJob. |
| |
| IorapdJobServiceProxy proxy = mProxy; |
| if (proxy != null) { |
| proxy.jobFinished(jobParameters, /*reschedule*/false); |
| } |
| // Cannot call 'jobFinished' on 'this' because it was not constructed |
| // from the JobService, so it would get an NPE when calling mEngine. |
| } |
| |
| public void onIorapdDisconnected() { |
| synchronized (mLock) { |
| mRunningJobs.clear(); |
| } |
| |
| if (DEBUG) { |
| Log.v(TAG, String.format("IorapdJobService#onIorapdDisconnected")); |
| } |
| |
| // TODO: should we try to resubmit all incomplete jobs after it's reconnected? |
| } |
| } |
| |
| private class RemoteTaskListener extends ITaskListener.Stub { |
| @Override |
| public void onProgress(RequestId requestId, TaskResult result) throws RemoteException { |
| if (DEBUG) { |
| Log.v(TAG, |
| String.format("RemoteTaskListener#onProgress(%s, %s)", requestId, result)); |
| } |
| |
| // TODO: implement rest. |
| } |
| |
| @Override |
| public void onComplete(RequestId requestId, TaskResult result) throws RemoteException { |
| if (DEBUG) { |
| Log.v(TAG, |
| String.format("RemoteTaskListener#onComplete(%s, %s)", requestId, result)); |
| } |
| |
| if (mJobService != null) { |
| mJobService.onIorapdTaskCompleted(requestId); |
| } |
| |
| // TODO: implement rest. |
| } |
| } |
| |
| /** Allow passing lambdas to #invokeRemote */ |
| private interface RemoteRunnable { |
| // TODO: run(RequestId) ? |
| void run(IIorap iorap) throws RemoteException; |
| } |
| |
| // Always pass in the iorap directly here to avoid data race. |
| private static boolean invokeRemote(IIorap iorap, RemoteRunnable r) { |
| if (iorap == null) { |
| Log.w(TAG, "IIorap went to null in this thread, drop invokeRemote."); |
| return false; |
| } |
| try { |
| r.run(iorap); |
| return true; |
| } catch (RemoteException e) { |
| // This could be a logic error (remote side returning error), which we need to fix. |
| // |
| // This could also be a DeadObjectException in which case its probably just iorapd |
| // being manually restarted. |
| // |
| // Don't make any assumption, since DeadObjectException could also mean iorapd crashed |
| // unexpectedly. |
| // |
| // DeadObjectExceptions are recovered from using DeathRecipient and #linkToDeath. |
| handleRemoteError(e); |
| return false; |
| } |
| } |
| |
| private static void handleRemoteError(Throwable t) { |
| if (WTF_CRASH) { |
| // In development modes, we just want to crash. |
| throw new AssertionError("unexpected remote error", t); |
| } else { |
| // Log to wtf which gets sent to dropbox, and in system_server this does not crash. |
| Log.wtf(TAG, t); |
| } |
| } |
| |
| // Encode A-Z bitstring into bits. Every character is bits. |
| // Characters outside of the range [a,z] are considered out of range. |
| // |
| // The least significant bits hold the last character. |
| // First 2 bits are left as 0. |
| private static int encodeEnglishAlphabetStringIntoInt(String name) { |
| int value = 0; |
| |
| final int CHARS_PER_INT = 6; |
| final int BITS_PER_CHAR = 5; |
| // Note: 2 top bits are unused, this also means our values are non-negative. |
| final char CHAR_LOWER = 'a'; |
| final char CHAR_UPPER = 'z'; |
| |
| if (name.length() > CHARS_PER_INT) { |
| throw new IllegalArgumentException( |
| "String too long. Cannot encode more than 6 chars: " + name); |
| } |
| |
| for (int i = 0; i < name.length(); ++i) { |
| char c = name.charAt(i); |
| |
| if (c < CHAR_LOWER || c > CHAR_UPPER) { |
| throw new IllegalArgumentException("String has out-of-range [a-z] chars: " + name); |
| } |
| |
| // Avoid sign extension during promotion. |
| int cur_value = (c & 0xFFFF) - (CHAR_LOWER & 0xFFFF); |
| if (cur_value >= (1 << BITS_PER_CHAR)) { |
| throw new AssertionError("wtf? i=" + i + ", name=" + name); |
| } |
| |
| value = value << BITS_PER_CHAR; |
| value = value | cur_value; |
| } |
| |
| return value; |
| } |
| } |